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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ internal class ServerComponentDeserializer
{
private readonly IDataProtector _dataProtector;
private readonly ILogger<ServerComponentDeserializer> _logger;
private readonly ServerComponentTypeCache _rootComponentTypeCache;
private readonly RootComponentTypeCache _rootComponentTypeCache;
private readonly ComponentParameterDeserializer _parametersDeserializer;

public ServerComponentDeserializer(
IDataProtectionProvider dataProtectionProvider,
ILogger<ServerComponentDeserializer> logger,
ServerComponentTypeCache rootComponentTypeCache,
RootComponentTypeCache rootComponentTypeCache,
ComponentParameterDeserializer parametersDeserializer)
{
// When we protect the data we use a time-limited data protector with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
services.TryAddSingleton<CircuitFactory>();
services.TryAddSingleton<ServerComponentDeserializer>();
services.TryAddSingleton<ServerComponentTypeCache>();
services.TryAddSingleton<RootComponentTypeCache>();
services.TryAddSingleton<ComponentParameterDeserializer>();
services.TryAddSingleton<ComponentParametersTypeCache>();
services.TryAddSingleton<CircuitIdFactory>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Description>Runtime server features for ASP.NET Core Components.</Description>
Expand Down Expand Up @@ -54,6 +54,8 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Circuits" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />

<Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ private ServerComponentDeserializer CreateServerComponentDeserializer()
return new ServerComponentDeserializer(
_ephemeralDataProtectionProvider,
NullLogger<ServerComponentDeserializer>.Instance,
new ServerComponentTypeCache(),
new RootComponentTypeCache(),
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Microsoft.AspNetCore.Components
{
// A cache for root component types
internal class ServerComponentTypeCache
internal class RootComponentTypeCache
{
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();

Expand Down Expand Up @@ -39,14 +39,14 @@ private static Type ResolveType(Key key, Assembly[] assemblies)
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
}

private struct Key : IEquatable<Key>
private readonly struct Key : IEquatable<Key>
{
public Key(string assembly, string type) =>
(Assembly, Type) = (assembly, type);

public string Assembly { get; set; }
public string Assembly { get; }

public string Type { get; set; }
public string Type { get; }

public override bool Equals(object obj) => Equals((Key)obj);

Expand Down
8 changes: 4 additions & 4 deletions src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/Components/Web.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { shouldAutoStart } from './BootCommon';
import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, Logger } from './Platform/Logging/Logger';
import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
import { discoverComponents, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';

let renderingFailed = false;
let started = false;
Expand All @@ -29,7 +30,7 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');

const components = discoverComponents(document);
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
const circuit = new CircuitDescriptor(components);

const initialConnection = await initializeConnection(options, logger, circuit);
Expand Down
29 changes: 27 additions & 2 deletions src/Components/Web.JS/src/Boot.WebAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
import './GlobalExports';
import * as Environment from './Environment';
import { monoPlatform } from './Platform/Mono/MonoPlatform';
import { renderBatch, getRendererer } from './Rendering/Renderer';
import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
import { shouldAutoStart } from './BootCommon';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
Expand All @@ -11,6 +11,8 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
import { BootConfigResult } from './Platform/BootConfig';
import { Pointer } from './Platform/Platform';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { discoverComponents, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';

let started = false;

Expand Down Expand Up @@ -71,8 +73,31 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
const environment = options?.environment;

// Fetch the resources and prepare the Mono runtime
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

// the document.
const discoveredComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
const componentAttacher = new WebAssemblyComponentAttacher(discoveredComponents);
window['Blazor']._internal.registeredComponents = {
getRegisteredComponentsCount: () => componentAttacher.getCount(),
getId: (index) => componentAttacher.getId(index),
getAssembly: (id) => BINDING.js_string_to_mono_string(componentAttacher.getAssembly(id)),
getTypeName: (id) => BINDING.js_string_to_mono_string(componentAttacher.getTypeName(id)),
getParameterDefinitions: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterDefinitions(id) || ''),
getParameterValues: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterValues(id) || ''),
};

window['Blazor']._internal.attachRootComponentToElement = (selector, componentId, rendererId) => {
const element = componentAttacher.resolveRegisteredElement(selector);
if (!element) {
attachRootComponentToElement(selector, componentId, rendererId);
} else {
attachRootComponentToLogicalElement(rendererId, element, componentId);
}
};

const bootConfigResult = await bootConfigPromise;
const [resourceLoader] = await Promise.all([
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
Expand Down
1 change: 0 additions & 1 deletion src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ window['Blazor'] = {
navigateTo,

_internal: {
attachRootComponentToElement,
navigationManager: navigationManagerInternalFunctions,
domWrapper: domFunctions,
Virtualize,
Expand Down
223 changes: 3 additions & 220 deletions src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
import { ServerComponentDescriptor } from '../../Services/ComponentDescriptorDiscovery';

export class CircuitDescriptor {
public circuitId?: string;

public components: ComponentDescriptor[];
public components: ServerComponentDescriptor[];

public constructor(components: ComponentDescriptor[]) {
public constructor(components: ServerComponentDescriptor[]) {
this.circuitId = undefined;
this.components = components;
}
Expand Down Expand Up @@ -54,221 +55,3 @@ export class CircuitDescriptor {
}
}

interface ComponentMarker {
type: string;
sequence: number;
descriptor: string;
}

export class ComponentDescriptor {
public type: string;

public start: Node;

public end?: Node;

public sequence: number;

public descriptor: string;

public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
this.type = type;
this.start = start;
this.end = end;
this.sequence = sequence;
this.descriptor = descriptor;
}

public toRecord(): ComponentMarker {
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
return result;
}
}

export function discoverComponents(document: Document): ComponentDescriptor[] {
const componentComments = resolveComponentComments(document);
const discoveredComponents: ComponentDescriptor[] = [];
for (let i = 0; i < componentComments.length; i++) {
const componentComment = componentComments[i];
const entry = new ComponentDescriptor(
componentComment.type,
componentComment.start,
componentComment.end,
componentComment.sequence,
componentComment.descriptor,
);

discoveredComponents.push(entry);
}

return discoveredComponents.sort((a, b) => a.sequence - b.sequence);
}


interface ComponentComment {
type: 'server';
sequence: number;
descriptor: string;
start: Node;
end?: Node;
prerenderId?: string;
}

function resolveComponentComments(node: Node): ComponentComment[] {
if (!node.hasChildNodes()) {
return [];
}

const result: ComponentComment[] = [];
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
while (childNodeIterator.next() && childNodeIterator.currentElement) {
const componentComment = getComponentComment(childNodeIterator);
if (componentComment) {
result.push(componentComment);
} else {
const childResults = resolveComponentComments(childNodeIterator.currentElement);
for (let j = 0; j < childResults.length; j++) {
const childResult = childResults[j];
result.push(childResult);
}
}
}

return result;
}

const blazorCommentRegularExpression = /\W*Blazor:[^{]*(.*)$/;

function getComponentComment(commentNodeIterator: ComponentCommentIterator): ComponentComment | undefined {
const candidateStart = commentNodeIterator.currentElement;

if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
return;
}
if (candidateStart.textContent) {
const componentStartComment = new RegExp(blazorCommentRegularExpression);
const definition = componentStartComment.exec(candidateStart.textContent);
const json = definition && definition[1];

if (json) {
try {
return createComponentComment(json, candidateStart, commentNodeIterator);
} catch (error) {
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
}
} else {
return;
}
}
}

function createComponentComment(json: string, start: Node, iterator: ComponentCommentIterator): ComponentComment {
const payload = JSON.parse(json) as ComponentComment;
const { type, sequence, descriptor, prerenderId } = payload;
if (type !== 'server') {
throw new Error(`Invalid component type '${type}'.`);
}

if (!descriptor) {
throw new Error('descriptor must be defined when using a descriptor.');
}

if (sequence === undefined) {
throw new Error('sequence must be defined when using a descriptor.');
}

if (!Number.isInteger(sequence)) {
throw new Error(`Error parsing the sequence '${sequence}' for component '${json}'`);
}

if (!prerenderId) {
return {
type,
sequence: sequence,
descriptor,
start,
};
} else {
const end = getComponentEndComment(prerenderId, iterator);
if (!end) {
throw new Error(`Could not find an end component comment for '${start}'`);
}

return {
type,
sequence,
descriptor,
start,
prerenderId,
end,
};
}
}

function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
while (iterator.next() && iterator.currentElement) {
const node = iterator.currentElement;
if (node.nodeType !== Node.COMMENT_NODE) {
continue;
}
if (!node.textContent) {
continue;
}

const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
const json = definition && definition[1];
if (!json) {
continue;
}

validateEndComponentPayload(json, prerenderedId);

return node;
}

return undefined;
}

function validateEndComponentPayload(json: string, prerenderedId: string): void {
const payload = JSON.parse(json) as ComponentComment;
if (Object.keys(payload).length !== 1) {
throw new Error(`Invalid end of component comment: '${json}'`);
}
const prerenderedEndId = payload.prerenderId;
if (!prerenderedEndId) {
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
}
if (prerenderedEndId !== prerenderedId) {
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
}
}

class ComponentCommentIterator {

private childNodes: NodeListOf<ChildNode>;

private currentIndex: number;

private length: number;

public currentElement: ChildNode | undefined;

public constructor(childNodes: NodeListOf<ChildNode>) {
this.childNodes = childNodes;
this.currentIndex = -1;
this.length = childNodes.length;
}

public next(): boolean {
this.currentIndex++;
if (this.currentIndex < this.length) {
this.currentElement = this.childNodes[this.currentIndex];
return true;
} else {
this.currentElement = undefined;
return false;
}
}
}


Loading