-
Notifications
You must be signed in to change notification settings - Fork 10.2k
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
Allow declaring render modes, and emit the corresponding markers into HTML #48190
Conversation
@@ -5,6 +5,7 @@ | |||
using System.Diagnostics.CodeAnalysis; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conceptually, the changes in this file are:
- Extend the existing per-component-type cache to also track the render mode declared on the component type, so there's no new runtime cost for determining this (not even an extra cache lookup)
- When a nonnull rendermode is found, use the new
ResolveComponentForRenderMode
to do the component instantiation. This is how a rendermode gets translated into concrete, platform-specific behavior.
{ | ||
var result = new Dictionary<string, object>(); | ||
var result = new Dictionary<string, object?>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nullability annotation was wrong before.
src/Components/Components/src/RenderTree/ComponentFrameFlags.cs
Outdated
Show resolved
Hide resolved
if (frame.ComponentStateField != null) | ||
{ | ||
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check was redundant since InstantiateChildComponentOnFrame
already does that.
// marker types into a single thing for auto mode (the code below emits both separately for auto). | ||
// It may be better to use a custom element like <blazor-component ...>[prerendered]<blazor-component> | ||
// so it's easier for the JS code to react automatically whenever this gets inserted or updated during | ||
// streaming SSR or progressively-enhanced navigation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this PR I'm not trying to express a final opinion on the exact format of the interactive component markers that we output. We can determine that when implementing the client-side pieces, based on what's best for the client-side code (e.g., whether it should change from being a comment tag to a custom element).
The goal for this PR is just to emit the output in some sensible format. The specific format used in this PR happens to be exactly what we already output, and hence is compatible with blazor.server.js
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue with emitting a custom element is that it will affect the layout, isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is a risk, and is why we haven't done it before. It is a bit of an edge case though, since <my-custom-container>some markup</my-custom-container>
will normally render exactly the same as some markup
, if there are no CSS rules associated with my-custom-container
. The cases where it would make a different would be:
- if you have very specific CSS rules that try to locate things in
some markup
based on them being immediate children of some other element - or if the parent is a special element type like
table
that can only have certain child types - There may be other such cases too.
We could decide that rendermode boundaries are rare enough that we are willing to tell people not to place them at critical locations (like as immediate children of <table>
), and not to write CSS rules that are so specific in this case.
It's a tradeoff between compatibility and simplicity of implementation, and warrants some open discussion. Overall I suspect we'll end up saying we want to maximize compatibility even if it makes our implementation harder. But we are free to discuss this and be open to other outcomes.
/// A component that describes a location in prerendered output where client-side code | ||
/// should insert an interactive component. | ||
/// </summary> | ||
internal class SSRRenderModeBoundary : IComponent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having the boundary be an actual component in the hierarchy means:
- It automatically captures the parameters that we need to pass to the interactive child, without any extra logic in the prerendering or rendering systems
- We can encapsulate things like "doing prerendering or not" with a trivial "if" condition here
- We can trivially use other DI services like Data Protection from here without adding noise to the renderer
- We can easily see if rendermodes are nested by scanning the ancestor hierarchy.
a3c0de4
to
390d7c3
Compare
Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute | ||
Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.RenderModeAutoAttribute() -> void | ||
Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.RenderModeAutoAttribute(bool prerender) -> void | ||
Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute | ||
Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.RenderModeServerAttribute() -> void | ||
Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.RenderModeServerAttribute(bool prerender) -> void | ||
Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute | ||
Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.RenderModeWebAssemblyAttribute() -> void | ||
Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.RenderModeWebAssemblyAttribute(bool prerender) -> void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these can be removed when the Razor compiler is updated to support the @rendermode directive.
override Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! | ||
override Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! | ||
override Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These can also be removed once we have @rendermode
Are we still considering supporting setting render mode via file extension too, e.g. |
Interesting idea. Previously I thought that the naming convention would only be to influence conditional compilation. But we can make the Razor compiler do whatever we like. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great to me!
builder.CloseComponent(); | ||
} | ||
|
||
public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just so I'm understanding this correctly - in the case of the "Auto" render mode, we'll emit markers of both types? And on the client side, we'll have some logic to detect if any given component has multiple markers and treat that as "auto" mode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in the auto case, the client has to make the choice about what mode to use (at least, if it's based on "is the WebAssembly .NET runtime ready to start"). We are free to change the format of these markers and perhaps come up with a format that merges the server and WebAssembly information into a single structure, but this PR is only about emitting markers in the existing format. We should design a new format, if we want one, based on the needs of the client-side code when we implement that.
src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Outdated
Show resolved
Hide resolved
src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Outdated
Show resolved
Hide resolved
ea22b9b
to
466f62c
Compare
/// a fixed rendering mode when the component is incapable of running in other modes. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Class)] | ||
public abstract class RenderModeAttribute : Attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to allow multiple for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. What would we do if there were multiple?
This is about specifying a required rendermode, not a set of allowed rendermodes (which, if implemented in the future, would be a separate API).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean AllowMultiple = false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the default. If I'm missing something and it has to be specified explicitly in this case, please let me know!
This is the basis for including interactive (Server or WebAssembly) components at arbitrary locations within SSR output. This PR only implements the SSR pieces, i.e., emitting the markers into the HTML. This PR does not include the client-side code needed to start up a circuit or WebAssembly runtime, so it is not yet usable as an end-to-end feature.
Usage advice
Render modes are defined statically on component types. Where possible, components should not specify a rendermode at all. This means they are willing to work on any hosting platform, or that the developer knows they will only be used in a compatible hosting platform. For example:
Restrictions
We're trying to keep the feature surface small where possible to begin with. So for .NET 8, we expect that:
API design
A rendermode is a class that implements
IComponentRenderMode
. The built-in ones areServerRenderMode
,WebAssemblyRenderMode
,AutoRenderMode
. These are the only rendermodes recognized by the built-in renderers for SSR, Server, and WebAssembly.Since rendermodes are arbitrary types, they can hold additional parameters (e.g., to control whether to prerender).
The set of rendermodes is not fixed as far as M.A.Components is concerned. In fact, the core doesn't know about any specific render modes. The ones we support today are all defined in M.A.Components.Web. Platforms with custom renderers can implement support for their own rendermodes by overriding
ResolveComponentForRenderMode
.Note that the existing
Html.RenderComponentAsync
and<component>
tag helpers already have their own differentRenderMode
enum. This PR automatically maps that to the newIComponentRenderMode
types, so existing APIs continue to work. Given the intended API design, I don't think the naming clash will affect real-world use, but we can see how this goes.Example usage
Final syntax (not yet supported by Razor compiler):
@rendermode Server // (or WebAssembly or Auto)
Temporary syntax until Razor compiler is updated:
@attribute [ServerRenderMode] // or [WebAssemblyRenderMode] or [AutoRenderMode]
Note that the ServerRenderMode/WebAssemblyRenderMode/AutoRenderMode attributes can all be removed once the Razor compiler is updated, because we can cause
@rendermode SOME_EXPRESSION_HERE
to compile as:... and then the project template's
_Imports.razor
can haveusing static Microsoft.AspNetCore.Components.Web.RenderMode
so that it is legal to simply write@rendermode Server
. This allows the cleanest possible syntax without coupling the Razor compiler to any particular M.A.C.Web render modes or naming conventions.Behavior
Currently, it emits
<!--Blazor:{...}-->
markers, including the serialized parameters, in a form compatible with the existingblazor.server.js
andblazor.webassembly.js
. The existing prerendering logic has been updated to use the new rendering mechanism.When we implement interactive component support in
blazor.web.js
, we can change this marker format if we want (but then would also have to updateblazor.server.js
andblazor.webassembly.js
).