Skip to content

Commit 402dc41

Browse files
authored
[Blazor] Render Blazor Webassembly components from MVC (#25203)
* Server pieces * Changes for prerendering * Discover client components * tmp * Cleanup * Cleanups * Undo changes * Remove unwanted changes * Move interop class to its own file * Cleanup unwanted changes * Add test rendering multiple client-side components * Unit tests and E2E tests * Cleanups * Addressed feedback * Rename Client to WebAssembly in RenderMode * Update generated js files * Cleaned up JS and addressed feedback * Client->WebAssembly and other feedback * Unify component discovery code and use webassembly instead of 'client' * Update js files * Fix tests
1 parent 78a587b commit 402dc41

File tree

42 files changed

+1301
-278
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1301
-278
lines changed

src/Components/Server/src/Circuits/ServerComponentDeserializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ internal class ServerComponentDeserializer
5959
{
6060
private readonly IDataProtector _dataProtector;
6161
private readonly ILogger<ServerComponentDeserializer> _logger;
62-
private readonly ServerComponentTypeCache _rootComponentTypeCache;
62+
private readonly RootComponentTypeCache _rootComponentTypeCache;
6363
private readonly ComponentParameterDeserializer _parametersDeserializer;
6464

6565
public ServerComponentDeserializer(
6666
IDataProtectionProvider dataProtectionProvider,
6767
ILogger<ServerComponentDeserializer> logger,
68-
ServerComponentTypeCache rootComponentTypeCache,
68+
RootComponentTypeCache rootComponentTypeCache,
6969
ComponentParameterDeserializer parametersDeserializer)
7070
{
7171
// When we protect the data we use a time-limited data protector with the

src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
5757
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
5858
services.TryAddSingleton<CircuitFactory>();
5959
services.TryAddSingleton<ServerComponentDeserializer>();
60-
services.TryAddSingleton<ServerComponentTypeCache>();
60+
services.TryAddSingleton<RootComponentTypeCache>();
6161
services.TryAddSingleton<ComponentParameterDeserializer>();
6262
services.TryAddSingleton<ComponentParametersTypeCache>();
6363
services.TryAddSingleton<CircuitIdFactory>();

src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
44
<Description>Runtime server features for ASP.NET Core Components.</Description>
@@ -54,6 +54,8 @@
5454
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
5555
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Circuits" />
5656
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
57+
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
58+
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
5759

5860
<Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
5961
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />

src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ private ServerComponentDeserializer CreateServerComponentDeserializer()
320320
return new ServerComponentDeserializer(
321321
_ephemeralDataProtectionProvider,
322322
NullLogger<ServerComponentDeserializer>.Instance,
323-
new ServerComponentTypeCache(),
323+
new RootComponentTypeCache(),
324324
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
325325
}
326326

src/Components/Server/src/Circuits/ServerComponentTypeCache.cs renamed to src/Components/Shared/src/RootComponentTypeCache.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
namespace Microsoft.AspNetCore.Components
1010
{
1111
// A cache for root component types
12-
internal class ServerComponentTypeCache
12+
internal class RootComponentTypeCache
1313
{
1414
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
1515

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

42-
private struct Key : IEquatable<Key>
42+
private readonly struct Key : IEquatable<Key>
4343
{
4444
public Key(string assembly, string type) =>
4545
(Assembly, Type) = (assembly, type);
4646

47-
public string Assembly { get; set; }
47+
public string Assembly { get; }
4848

49-
public string Type { get; set; }
49+
public string Type { get; }
5050

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

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { shouldAutoStart } from './BootCommon';
77
import { RenderQueue } from './Platform/Circuits/RenderQueue';
88
import { ConsoleLogger } from './Platform/Logging/Loggers';
99
import { LogLevel, Logger } from './Platform/Logging/Logger';
10-
import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
10+
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
1111
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
1212
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
1313
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
1414
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
15+
import { discoverComponents, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
1516

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

32-
const components = discoverComponents(document);
33+
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
3334
const circuit = new CircuitDescriptor(components);
3435

3536
const initialConnection = await initializeConnection(options, logger, circuit);

src/Components/Web.JS/src/Boot.WebAssembly.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
22
import './GlobalExports';
33
import * as Environment from './Environment';
44
import { monoPlatform } from './Platform/Mono/MonoPlatform';
5-
import { renderBatch, getRendererer } from './Rendering/Renderer';
5+
import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
66
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
77
import { shouldAutoStart } from './BootCommon';
88
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
@@ -11,6 +11,8 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
1111
import { BootConfigResult } from './Platform/BootConfig';
1212
import { Pointer } from './Platform/Platform';
1313
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
14+
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
15+
import { discoverComponents, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
1416

1517
let started = false;
1618

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

7375
// Fetch the resources and prepare the Mono runtime
74-
const bootConfigResult = await BootConfigResult.initAsync(environment);
76+
const bootConfigPromise = BootConfigResult.initAsync(environment);
77+
78+
// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
79+
// the document.
80+
const discoveredComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
81+
const componentAttacher = new WebAssemblyComponentAttacher(discoveredComponents);
82+
window['Blazor']._internal.registeredComponents = {
83+
getRegisteredComponentsCount: () => componentAttacher.getCount(),
84+
getId: (index) => componentAttacher.getId(index),
85+
getAssembly: (id) => BINDING.js_string_to_mono_string(componentAttacher.getAssembly(id)),
86+
getTypeName: (id) => BINDING.js_string_to_mono_string(componentAttacher.getTypeName(id)),
87+
getParameterDefinitions: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterDefinitions(id) || ''),
88+
getParameterValues: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterValues(id) || ''),
89+
};
90+
91+
window['Blazor']._internal.attachRootComponentToElement = (selector, componentId, rendererId) => {
92+
const element = componentAttacher.resolveRegisteredElement(selector);
93+
if (!element) {
94+
attachRootComponentToElement(selector, componentId, rendererId);
95+
} else {
96+
attachRootComponentToLogicalElement(rendererId, element, componentId);
97+
}
98+
};
7599

100+
const bootConfigResult = await bootConfigPromise;
76101
const [resourceLoader] = await Promise.all([
77102
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
78103
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);

src/Components/Web.JS/src/GlobalExports.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ window['Blazor'] = {
88
navigateTo,
99

1010
_internal: {
11-
attachRootComponentToElement,
1211
navigationManager: navigationManagerInternalFunctions,
1312
domWrapper: domFunctions,
1413
Virtualize,
Lines changed: 3 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
22
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
3+
import { ServerComponentDescriptor } from '../../Services/ComponentDescriptorDiscovery';
34

45
export class CircuitDescriptor {
56
public circuitId?: string;
67

7-
public components: ComponentDescriptor[];
8+
public components: ServerComponentDescriptor[];
89

9-
public constructor(components: ComponentDescriptor[]) {
10+
public constructor(components: ServerComponentDescriptor[]) {
1011
this.circuitId = undefined;
1112
this.components = components;
1213
}
@@ -54,221 +55,3 @@ export class CircuitDescriptor {
5455
}
5556
}
5657

57-
interface ComponentMarker {
58-
type: string;
59-
sequence: number;
60-
descriptor: string;
61-
}
62-
63-
export class ComponentDescriptor {
64-
public type: string;
65-
66-
public start: Node;
67-
68-
public end?: Node;
69-
70-
public sequence: number;
71-
72-
public descriptor: string;
73-
74-
public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
75-
this.type = type;
76-
this.start = start;
77-
this.end = end;
78-
this.sequence = sequence;
79-
this.descriptor = descriptor;
80-
}
81-
82-
public toRecord(): ComponentMarker {
83-
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
84-
return result;
85-
}
86-
}
87-
88-
export function discoverComponents(document: Document): ComponentDescriptor[] {
89-
const componentComments = resolveComponentComments(document);
90-
const discoveredComponents: ComponentDescriptor[] = [];
91-
for (let i = 0; i < componentComments.length; i++) {
92-
const componentComment = componentComments[i];
93-
const entry = new ComponentDescriptor(
94-
componentComment.type,
95-
componentComment.start,
96-
componentComment.end,
97-
componentComment.sequence,
98-
componentComment.descriptor,
99-
);
100-
101-
discoveredComponents.push(entry);
102-
}
103-
104-
return discoveredComponents.sort((a, b) => a.sequence - b.sequence);
105-
}
106-
107-
108-
interface ComponentComment {
109-
type: 'server';
110-
sequence: number;
111-
descriptor: string;
112-
start: Node;
113-
end?: Node;
114-
prerenderId?: string;
115-
}
116-
117-
function resolveComponentComments(node: Node): ComponentComment[] {
118-
if (!node.hasChildNodes()) {
119-
return [];
120-
}
121-
122-
const result: ComponentComment[] = [];
123-
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
124-
while (childNodeIterator.next() && childNodeIterator.currentElement) {
125-
const componentComment = getComponentComment(childNodeIterator);
126-
if (componentComment) {
127-
result.push(componentComment);
128-
} else {
129-
const childResults = resolveComponentComments(childNodeIterator.currentElement);
130-
for (let j = 0; j < childResults.length; j++) {
131-
const childResult = childResults[j];
132-
result.push(childResult);
133-
}
134-
}
135-
}
136-
137-
return result;
138-
}
139-
140-
const blazorCommentRegularExpression = /\W*Blazor:[^{]*(.*)$/;
141-
142-
function getComponentComment(commentNodeIterator: ComponentCommentIterator): ComponentComment | undefined {
143-
const candidateStart = commentNodeIterator.currentElement;
144-
145-
if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
146-
return;
147-
}
148-
if (candidateStart.textContent) {
149-
const componentStartComment = new RegExp(blazorCommentRegularExpression);
150-
const definition = componentStartComment.exec(candidateStart.textContent);
151-
const json = definition && definition[1];
152-
153-
if (json) {
154-
try {
155-
return createComponentComment(json, candidateStart, commentNodeIterator);
156-
} catch (error) {
157-
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
158-
}
159-
} else {
160-
return;
161-
}
162-
}
163-
}
164-
165-
function createComponentComment(json: string, start: Node, iterator: ComponentCommentIterator): ComponentComment {
166-
const payload = JSON.parse(json) as ComponentComment;
167-
const { type, sequence, descriptor, prerenderId } = payload;
168-
if (type !== 'server') {
169-
throw new Error(`Invalid component type '${type}'.`);
170-
}
171-
172-
if (!descriptor) {
173-
throw new Error('descriptor must be defined when using a descriptor.');
174-
}
175-
176-
if (sequence === undefined) {
177-
throw new Error('sequence must be defined when using a descriptor.');
178-
}
179-
180-
if (!Number.isInteger(sequence)) {
181-
throw new Error(`Error parsing the sequence '${sequence}' for component '${json}'`);
182-
}
183-
184-
if (!prerenderId) {
185-
return {
186-
type,
187-
sequence: sequence,
188-
descriptor,
189-
start,
190-
};
191-
} else {
192-
const end = getComponentEndComment(prerenderId, iterator);
193-
if (!end) {
194-
throw new Error(`Could not find an end component comment for '${start}'`);
195-
}
196-
197-
return {
198-
type,
199-
sequence,
200-
descriptor,
201-
start,
202-
prerenderId,
203-
end,
204-
};
205-
}
206-
}
207-
208-
function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
209-
while (iterator.next() && iterator.currentElement) {
210-
const node = iterator.currentElement;
211-
if (node.nodeType !== Node.COMMENT_NODE) {
212-
continue;
213-
}
214-
if (!node.textContent) {
215-
continue;
216-
}
217-
218-
const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
219-
const json = definition && definition[1];
220-
if (!json) {
221-
continue;
222-
}
223-
224-
validateEndComponentPayload(json, prerenderedId);
225-
226-
return node;
227-
}
228-
229-
return undefined;
230-
}
231-
232-
function validateEndComponentPayload(json: string, prerenderedId: string): void {
233-
const payload = JSON.parse(json) as ComponentComment;
234-
if (Object.keys(payload).length !== 1) {
235-
throw new Error(`Invalid end of component comment: '${json}'`);
236-
}
237-
const prerenderedEndId = payload.prerenderId;
238-
if (!prerenderedEndId) {
239-
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
240-
}
241-
if (prerenderedEndId !== prerenderedId) {
242-
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
243-
}
244-
}
245-
246-
class ComponentCommentIterator {
247-
248-
private childNodes: NodeListOf<ChildNode>;
249-
250-
private currentIndex: number;
251-
252-
private length: number;
253-
254-
public currentElement: ChildNode | undefined;
255-
256-
public constructor(childNodes: NodeListOf<ChildNode>) {
257-
this.childNodes = childNodes;
258-
this.currentIndex = -1;
259-
this.length = childNodes.length;
260-
}
261-
262-
public next(): boolean {
263-
this.currentIndex++;
264-
if (this.currentIndex < this.length) {
265-
this.currentElement = this.childNodes[this.currentIndex];
266-
return true;
267-
} else {
268-
this.currentElement = undefined;
269-
return false;
270-
}
271-
}
272-
}
273-
274-

0 commit comments

Comments
 (0)