Skip to content

[Blazor] Render Blazor Webassembly components from MVC #25203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

javiercn
Copy link
Member

@javiercn javiercn commented Aug 24, 2020

Description

We want to enable rendering blazor webassembly components from MVC applications, this means rendering a placeholder for a component as well
as prerendering a component that gets re-rendered client-side when the Blazor webassembly application starts.

Customer Impact

  • Blazor WebAssembly apps typically have a slower load time due to the size of the download. Prerendering is a common mitigation to speed up the perceived load time
  • Also enables progressively adding client-side functionality and interactivity using Blazor components to MVC/Razor Pages based apps

Regression?

No

Risk

Low. We are enabling the same functionality that existed for server-side Blazor application with the same level of support. The code is shared to a large degree with the existing server-side Blazor support and follows the same approach, with a similar set of supported scenarios.

There hasn't been relevant feedback since 3.1 about the support we have in server-side Blazor, so the features we offer cover the majority of scenarios our customers care about.

There has been trickling feedback from customers about the desire to do this, so we decided to do a stretch and include it last minute, since the functionality and scope is well understood and covered by an existing group of tests.

@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Aug 24, 2020
@javiercn javiercn marked this pull request as ready for review August 24, 2020 20:50
@javiercn javiercn requested review from SteveSandersonMS and a team as code owners August 24, 2020 20:50
Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good

const payload = JSON.parse(json) as ComponentComment;
const { type, sequence, descriptor, prerenderId } = payload;
if (type !== 'server') {
if (type !== 'server' && type !== 'client') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would CircuitManager encounter this? Could you add a comment for this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have a page that renders both a server component and a client component, I guess we don't support that, but it's better to be defensive I think.

var registeredComponents = new ClientComponentMarker[componentsCount];
for (var i = 0; i < componentsCount; i++)
{
var id = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, int>(RegisteredComponentsInterop.GetId, i, null, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to read these in one call?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK I believe we can't. I know there are functions to read from the heap, but I don't think there are functions to write to the heap. Are there?

I don't think this is incredibly critical since unmarshalled interop should be pretty fast, I'm happy to change this post RC1 if we think it's important.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't feel strongly about this, as it's reasonably clear what's going on from the code.

If we wanted to simplify it on the .NET side, we might consider having a single marshalled call that returns the whole set of descriptors. The extra perf impact of that would be marginal because we're doing JSON deserialization for the parameters anyway. Marshalled calls are only slower because of the JSON aspect - in other respects they are basically the same as unmarshalled calls.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd have to measure it to be certain, but it might actually be faster as a single marshalled call here because we'd only have to go through the process of copying string data from JS to .NET once, instead of 5x for each root component.

However I still don't think it's urgent to optimize this.

@javiercn
Copy link
Member Author

🆙 📅

throw new Error(`Invalid component type '${type}'.`);
}

if(type === 'client')
{
return undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw your comment above about potentially supporting rendering both client and server components in the future. In the meantime, should this actually throw instead of returning?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s safer to ignore it I think, the idea here is that you only handle what you know about and leave anything else alone.


function validateEndComponentPayload(json: string, prerenderedId: string): void {
const payload = JSON.parse(json) as ComponentComment;
if (Object.keys(payload).length !== 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't expect the end comment to have all the properties of a typical component comment (e.g. type and parameterDefinitions) is it fair to type it like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern as the ServerComponentMarker on CircuitManager.ts

If I remember correctly, it was more problematic typing to deal with two different types.

There’s no much value in having an extra type just for this and it complicates signatures and other types.

@javiercn javiercn force-pushed the javiercn/blazor-server-render-webassembly-components branch from 48c5aa4 to be4ef1a Compare August 25, 2020 07:11
const bootConfigResult = await BootConfigResult.initAsync(environment);
const bootConfigPromise = BootConfigResult.initAsync(environment);

// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool

<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
<Compile Include="$(SharedSourceRoot)Components\ClientComponentSerializationSettings.cs" Link="Prerendering/ClientComponentSerializationSettings.cs" />
<Compile Include="$(SharedSourceRoot)Components\ClientComponentMarker.cs" Link="Prerendering/ClientComponentMarker.cs" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are ClientComponentSerializationSettings and ClientComponentMarker used for both server-side components and webassembly ones? It seems like probably so since they are now in shared source, but I can't see any corresponding older server-specific versions of the code having been removed in favour of the new shared ones.

Or if these are wasm-specific, would it be possible rename them to WebAssemblyComponentSerializationSettings and WebAssemblyComponentMarker? We've been preferring webassembly as the general term rather than "client" due to it being less ambiguous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed them.

I'm keeping the markers separate between SSB and Wasm. While they share some things in common, they represent implementation details so it's better to keep them separate to avoid accidental changes.

@SteveSandersonMS SteveSandersonMS self-requested a review August 25, 2020 11:51
Copy link
Member

@SteveSandersonMS SteveSandersonMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

We discussed offline and agreed on a bit of refactoring to consolidate the TypeScript code. Besides that, approving :)

@javiercn javiercn force-pushed the javiercn/blazor-server-render-webassembly-components branch from a7dfd7b to a992114 Compare August 25, 2020 12:51
@pranavkm pranavkm added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Aug 25, 2020
@ghost
Copy link

ghost commented Aug 25, 2020

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@pranavkm pranavkm added this to the 5.0.0-rc1 milestone Aug 25, 2020
@javiercn javiercn added the ask-mode This issue / PR is a patch candidate which we will bar-check internally before patching it. label Aug 25, 2020
@javiercn
Copy link
Member Author

Summary

We want to enable rendering blazor webassembly components from MVC applications, this means rendering a placeholder for a component as well
as prerendering a component that gets re-rendered client-side when the Blazor webassembly application starts.

Motivation

We've received feedback from multiple customers indicating that this is something they want to be able to do. In addition to that, this feature enables a few scenarios that are currently hard or impossible to achieve:

  • Progressively convert an MVC/Razor pages application from the bottom up.
  • Render a variable number of components on the page dynamically.
  • Avoid the need use an element in markup to define a root component for a Blazor application.

Goals

  • Make it possible to render blazor webassembly components from MVC applications.
  • Make it possible to pass parameters to the root component in a Blazor Webassembly application from an MVC page.

Non-goals

  • Dynamically render components after the application has started.
  • Support all types of parameters, specifically the following won't be supported (which is inline with server-side Blazor):
    • Generic parameters
    • Non-serializable parameters
    • Inheritance in collection parameters
    • Parameters whose type is defined outside of the Blazor WebAssembly application or within a lazily loaded assembly.

Scenarios

  • Render a blazor Webassembly component from Razor.
  • Prerender a blazor Webassembly component from Razor.
  • Render a blazor webassembly component with parameters from Razor.
  • Prerender a Blazor webassembly component with parameters from Razor.
  • Render zero or more Blazor webassembly components from an MVC from Razor.

Risks

  • There are no particular risks with this feature since the behavior is very well defined on server-side Blazor and we haven't received significant feedback about the restrictions imposed during parameter serialization.

Interaction with other parts of the framework

  • Lazy loading: Rendering a root component on a lazy loaded assembly will not be supported.
  • Routing: The same restrictions that apply for SSB apply here, you should have at most one component that uses a router to prevent undefined behavior.
  • Progressive Web Apps: It will work fine provided that the set of arguments you pass doesn't change very often or based on user input. It is best to use a static page as the entry point for the PWA, but a Razor page can also be used if it mostly behaves as a static page.

Detailed design

The component tag helper will accept two new render modes:

  • Client
  • ClientPrerendered
<component Type="typeof(App)" RenderMode="Client" />
<component Type="typeof(App)" RenderMode="ClientPrerendered" param-Age="18" />

A comment with a descriptor will be emitted for each component that gets rendered into the page, similar to what is done for server-side Blazor.

Non prerendered components are represented by a single comment containing the descriptor:

<!--Blazor:{"type":"client","assembly":"BasicTestApp","typeName":"BasicTestApp.MultipleComponents.GreeterComponent","parameterDefinitions":"W3sibmFtZSI6Im5hbWUiLCJ0eXBlTmFtZSI6IlN5c3RlbS5TdHJpbmciLCJhc3NlbWJseSI6IlN5c3RlbS5Qcml2YXRlLkNvcmVMaWIifV0=","parameterValues":"WyJSZWQgZmlzaCJd"}-->

Prerendered components are represented by a pair of comments that surround the prerendered content and that include a matching "prerenderId":

<!--Blazor:{"type":"client","assembly":"BasicTestApp","typeName":"BasicTestApp.MultipleComponents.GreeterComponent","parameterDefinitions":"W3sibmFtZSI6Im5hbWUiLCJ0eXBlTmFtZSI6IlN5c3RlbS5TdHJpbmciLCJhc3NlbWJseSI6IlN5c3RlbS5Qcml2YXRlLkNvcmVMaWIifV0=","parameterValues":"WyJCbHVlIGZpc2giXQ==","prerenderId":"f005b7adf03f41febb9ec9195b96e342"}-->
<div class="greet-wrapper"><h3 class="">Greeter component</h3>
    <p class="greet">Hello Blue fish</p></div>
<!--Blazor:{"prerenderId":"f005b7adf03f41febb9ec9195b96e342"}-->

Each descriptor contains a type, that differentiates it from other descriptor types (server), the assembly name for the root component, the full type name of the root component, and the list of parameter definitions and parameters as JSON serialized and Base64 encoded strings.

Base64 encoding is necessary to ensure that the parameters and their definitions are valid HTML comments, given that parameter values can contain user input. For example, a string parameter with the value -- would result in an invalid HTML Comment.

When the Blazor webassembly application loads, it finds all the component descriptors on the page and gives each one a unique ID. When the .NET code boots up, it queries the JS Boot code to check if there are "registered" components, and if there are, it retrieves each component descriptor through JS interop and adds it to the list of RootComponents in the WebAssemblyHostBuilder.

The application has a chance to see the root components that were registered and can perform any tweaks or modifications necessary. As a side
effect of newly added APIs it is also now possible to pass parameters to the root component from within Program.cs

Drawbacks

Considered alternatives

Open questions

@mkArtakMSFT mkArtakMSFT merged commit 402dc41 into release/5.0 Aug 25, 2020
@mkArtakMSFT mkArtakMSFT deleted the javiercn/blazor-server-render-webassembly-components branch August 25, 2020 16:30
@pranavkm pranavkm removed the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components ask-mode This issue / PR is a patch candidate which we will bar-check internally before patching it.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants