Skip to content
代码片段 群组 项目
未验证 提交 0ca2ed9a 编辑于 作者: Javier Calvarro Nelson's avatar Javier Calvarro Nelson 提交者: GitHub
浏览文件

[Blazor] Custom JS initializers (#34798)

* Users can create an ES6 module with `<packageName>.lib.module.js` and declare a beforeStart and afterStarted function to integrate into the blazor start process.
* In WebAssembly beforeStart receives the BlazorWebAssemblyStartOptions object and any publish extension as part of the method arguments.
* In Server beforeStart receives the CircuitStartOptions object and can configure any necessary details.
* In Desktop beforeStart does not receive any argument for the time being.
* afterStarted receives the Blazor object as argument in all cases.
* Callbacks in beforeStart and afterStarted are not invoked in any defined order and are invoked in parallel.
上级 e1aeac80
No related branches found
No related tags found
无相关合并请求
显示
224 个添加19 个删除
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Rendering;
......@@ -95,6 +96,10 @@ namespace Microsoft.AspNetCore.Components.Authorization
builder.CloseComponent();
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:RequiresUnreferencedCode",
Justification = "OpenComponent already has the right set of attributes")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2110:RequiresUnreferencedCode",
Justification = "OpenComponent already has the right set of attributes")]
private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content)
{
builder.OpenComponent<LayoutView>(0);
......
......@@ -12,11 +12,13 @@ namespace Microsoft.AspNetCore.Builder
{
private readonly IEndpointConventionBuilder _hubEndpoint;
private readonly IEndpointConventionBuilder _disconnectEndpoint;
private readonly IEndpointConventionBuilder _jsInitializersEndpoint;
internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint)
internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint, IEndpointConventionBuilder jsInitializersEndpoint)
{
_hubEndpoint = hubEndpoint;
_disconnectEndpoint = disconnectEndpoint;
_jsInitializersEndpoint = jsInitializersEndpoint;
}
/// <summary>
......@@ -27,6 +29,7 @@ namespace Microsoft.AspNetCore.Builder
{
_hubEndpoint.Add(convention);
_disconnectEndpoint.Add(convention);
_jsInitializersEndpoint.Add(convention);
}
}
}
......@@ -3,9 +3,12 @@
using System;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.AspNetCore.Builder
{
......@@ -110,7 +113,12 @@ namespace Microsoft.AspNetCore.Builder
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
.WithDisplayName("Blazor disconnect");
return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint);
var jsInitializersEndpoint = endpoints.Map(
(path.EndsWith('/') ? path : path + "/") + "initializers/",
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitJavaScriptInitializationMiddleware>().Build())
.WithDisplayName("Blazor initializers");
return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint);
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
internal class CircuitJavaScriptInitializationMiddleware
{
private readonly IList<string> _initializers;
// We don't need the request delegate for anything, however we need to inject it to satisfy the middleware
// contract.
public CircuitJavaScriptInitializationMiddleware(IOptions<CircuitOptions> options, RequestDelegate _)
{
_initializers = options.Value.JavaScriptInitializers;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsJsonAsync(_initializers);
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Hosting;
namespace Microsoft.AspNetCore.Components.Server
{
......@@ -82,5 +81,7 @@ namespace Microsoft.AspNetCore.Components.Server
/// Gets options for root components within the circuit.
/// </summary>
public CircuitRootComponentOptions RootComponents { get; } = new CircuitRootComponentOptions();
internal IList<string> JavaScriptInitializers { get; } = new List<string>();
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitOptionsJavaScriptInitializersConfiguration : IConfigureOptions<CircuitOptions>
{
private readonly IWebHostEnvironment _environment;
public CircuitOptionsJavaScriptInitializersConfiguration(IWebHostEnvironment environment)
{
_environment = environment;
}
public void Configure(CircuitOptions options)
{
var file = _environment.WebRootFileProvider.GetFileInfo($"{_environment.ApplicationName}.modules.json");
if (file.Exists)
{
var initializers = JsonSerializer.Deserialize<string[]>(file.CreateReadStream());
for (var i = 0; i < initializers.Length; i++)
{
var initializer = initializers[i];
options.JavaScriptInitializers.Add(initializer);
}
}
}
}
}
......@@ -13,6 +13,7 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Components.Server
{
......
......@@ -83,6 +83,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJavaScriptInitializersConfiguration>());
if (configure != null)
{
......
......@@ -2,10 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Moq;
using Xunit;
......@@ -54,6 +55,9 @@ namespace Microsoft.AspNetCore.Components.Server.Tests
private IApplicationBuilder CreateAppBuilder()
{
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(e => e.ApplicationName).Returns("app");
environment.SetupGet(e => e.WebRootFileProvider).Returns(new NullFileProvider());
var services = new ServiceCollection();
services.AddSingleton(Mock.Of<IHostApplicationLifetime>());
services.AddLogging();
......@@ -64,6 +68,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests
services.AddRouting();
services.AddSignalR();
services.AddServerSideBlazor();
services.AddSingleton(environment.Object);
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
var serviceProvider = services.BuildServiceProvider();
......
文件被 .gitattributes 条目压制或文件的编码不受支持。
文件被 .gitattributes 条目压制或文件的编码不受支持。
......@@ -13,6 +13,7 @@ import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnect
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
import { discoverComponents, discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop';
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server';
let renderingFailed = false;
let started = false;
......@@ -25,6 +26,8 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
// Establish options to be used
const options = resolveOptions(userOptions);
const jsInitializer = await fetchAndInvokeInitializers(options);
const logger = new ConsoleLogger(options.logLevel);
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
......@@ -35,7 +38,6 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
const appState = discoverPersistedState(document);
const circuit = new CircuitDescriptor(components, appState || '');
const initialConnection = await initializeConnection(options, logger, circuit);
const circuitStarted = await circuit.startCircuit(initialConnection);
if (!circuitStarted) {
......@@ -77,6 +79,8 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
Blazor.reconnect = reconnect;
logger.log(LogLevel.Information, 'Blazor server-side application started.');
jsInitializer.invokeAfterStartedCallbacks(Blazor);
}
async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> {
......
......@@ -14,6 +14,8 @@ import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { discoverComponents, discoverPersistedState, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
import { setDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods';
import { AfterBlazorStartedCallback, JSInitializer } from './JSInitializers/JSInitializers';
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.WebAssembly';
declare var Module: EmscriptenModule;
let started = false;
......@@ -80,11 +82,13 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
);
});
// Get the custom environment setting if defined
const environment = options?.environment;
const candidateOptions = options ?? {};
// Get the custom environment setting and blazorBootJson loader if defined
const environment = candidateOptions.environment;
// Fetch the resources and prepare the Mono runtime
const bootConfigPromise = BootConfigResult.initAsync(environment);
const bootConfigPromise = BootConfigResult.initAsync(candidateOptions.loadBootResource, environment);
// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
// the document.
......@@ -110,9 +114,11 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
}
};
const bootConfigResult = await bootConfigPromise;
const bootConfigResult: BootConfigResult = await bootConfigPromise;
const jsInitializer = await fetchAndInvokeInitializers(bootConfigResult.bootConfig, candidateOptions);
const [resourceLoader] = await Promise.all([
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, candidateOptions || {}),
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
try {
......@@ -123,6 +129,9 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
// Start up the application
platform.callEntryPoint(resourceLoader.bootConfig.entryAssembly);
// At this point .NET has been initialized (and has yielded), we can't await the promise becasue it will
// only end when the app finishes running
jsInitializer.invokeAfterStartedCallbacks(Blazor);
}
function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any): any {
......
......@@ -4,6 +4,7 @@ import { shouldAutoStart } from './BootCommon';
import { internalFunctions as navigationManagerFunctions } from './Services/NavigationManager';
import { startIpcReceiver } from './Platform/WebView/WebViewIpcReceiver';
import { sendAttachPage, sendBeginInvokeDotNetFromJS, sendEndInvokeJSFromDotNet, sendByteArray, sendLocationChanged } from './Platform/WebView/WebViewIpcSender';
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.WebView';
let started = false;
......@@ -13,6 +14,8 @@ async function boot(): Promise<void> {
}
started = true;
const jsInitializer = await fetchAndInvokeInitializers();
startIpcReceiver();
DotNet.attachDispatcher({
......@@ -25,6 +28,7 @@ async function boot(): Promise<void> {
navigationManagerFunctions.listenForNavigationEvents(sendLocationChanged);
sendAttachPage(navigationManagerFunctions.getBaseURI(), navigationManagerFunctions.getLocationHref());
await jsInitializer.invokeAfterStartedCallbacks(Blazor);
}
Blazor.start = boot;
......
import { BootJsonData } from "../Platform/BootConfig";
import { CircuitStartOptions } from "../Platform/Circuits/CircuitStartOptions";
import { JSInitializer } from "./JSInitializers";
export async function fetchAndInvokeInitializers(options: Partial<CircuitStartOptions>) : Promise<JSInitializer> {
const jsInitializersResponse = await fetch('_blazor/initializers', {
method: 'GET',
credentials: 'include',
cache: 'no-cache'
});
const initializers: string[] = await jsInitializersResponse.json();
const jsInitializer = new JSInitializer();
await jsInitializer.importInitializersAsync(initializers, [options]);
return jsInitializer;
}
import { BootJsonData } from "../Platform/BootConfig";
import { WebAssemblyStartOptions } from "../Platform/WebAssemblyStartOptions";
import { JSInitializer } from "./JSInitializers";
export async function fetchAndInvokeInitializers(bootConfig: BootJsonData, options: Partial<WebAssemblyStartOptions>) : Promise<JSInitializer> {
const initializers = bootConfig.resources.libraryInitializers;
const jsInitializer = new JSInitializer();
if (initializers) {
await jsInitializer.importInitializersAsync(
Object.keys(initializers),
[options, bootConfig.resources.extensions]);
}
return jsInitializer;
}
import { JSInitializer } from "./JSInitializers";
export async function fetchAndInvokeInitializers() : Promise<JSInitializer> {
const jsInitializersResponse = await fetch('_framework/blazor.modules.json', {
method: 'GET',
credentials: 'include',
cache: 'no-cache'
});
const initializers: string[] = await jsInitializersResponse.json();
const jsInitializer = new JSInitializer();
await jsInitializer.importInitializersAsync(initializers, []);
return jsInitializer;
}
import { Blazor } from '../GlobalExports';
type BeforeBlazorStartedCallback = (...args: unknown[]) => Promise<void>;
export type AfterBlazorStartedCallback = (blazor: typeof Blazor) => Promise<void>;
type BlazorInitializer = { beforeStart: BeforeBlazorStartedCallback, afterStarted: AfterBlazorStartedCallback };
export class JSInitializer {
private afterStartedCallbacks: AfterBlazorStartedCallback[] = [];
async importInitializersAsync(initializerFiles: string[], initializerArguments: unknown[]): Promise<void> {
await Promise.all(initializerFiles.map(f => importAndInvokeInitializer(this, f)));
function adjustPath(path: string): string {
// This is the same we do in JS interop with the import callback
const base = document.baseURI;
path = base.endsWith('/') ? `${base}${path}` : `${base}/${path}`;
return path;
}
async function importAndInvokeInitializer(jsInitializer: JSInitializer, path: string): Promise<void> {
const adjustedPath = adjustPath(path);
const initializer = await import(/* webpackIgnore: true */ adjustedPath) as Partial<BlazorInitializer>;
if (initializer === undefined) {
return;
}
const { beforeStart: beforeStart, afterStarted: afterStarted } = initializer;
if (afterStarted) {
jsInitializer.afterStartedCallbacks.push(afterStarted);
}
if (beforeStart) {
return beforeStart(...initializerArguments);
}
}
}
async invokeAfterStartedCallbacks(blazor: typeof Blazor) {
await Promise.all(this.afterStartedCallbacks.map(callback => callback(blazor)));
}
}
import { WebAssemblyBootResourceType } from './WebAssemblyStartOptions';
type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) =>
string | Promise<Response> | null | undefined;
export class BootConfigResult {
private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) {
}
static async initAsync(environment?: string): Promise<BootConfigResult> {
const bootConfigResponse = await fetch('_framework/blazor.boot.json', {
method: 'GET',
credentials: 'include',
cache: 'no-cache'
});
static async initAsync(loadBootResource?: LoadBootResourceCallback, environment?: string): Promise<BootConfigResult> {
const loaderResponse = loadBootResource !== undefined ?
loadBootResource('manifest', 'blazor.boot.json', '_framework/blazor.boot.json', '') :
defaultLoadBlazorBootJson('_framework/blazor.boot.json');
const bootConfigResponse = loaderResponse instanceof Promise ?
await loaderResponse :
await defaultLoadBlazorBootJson(loaderResponse ?? '_framework/blazor.boot.json');
// While we can expect an ASP.NET Core hosted application to include the environment, other
// hosts may not. Assume 'Production' in the absence of any specified value.
......@@ -16,7 +23,16 @@ export class BootConfigResult {
bootConfig.modifiableAssemblies = bootConfigResponse.headers.get('DOTNET-MODIFIABLE-ASSEMBLIES');
return new BootConfigResult(bootConfig, applicationEnvironment);
async function defaultLoadBlazorBootJson(url: string) : Promise<Response> {
return fetch(url, {
method: 'GET',
credentials: 'include',
cache: 'no-cache'
});
}
};
}
// Keep in sync with bootJsonData from the BlazorWebAssemblySDK
......@@ -34,12 +50,16 @@ export interface BootJsonData {
modifiableAssemblies: string | null;
}
export type BootJsonDataExtension = { [extensionName: string]: ResourceList };
export interface ResourceGroups {
readonly assembly: ResourceList;
readonly lazyAssembly: ResourceList;
readonly pdb?: ResourceList;
readonly runtime: ResourceList;
readonly satelliteResources?: { [cultureName: string] : ResourceList };
readonly satelliteResources?: { [cultureName: string]: ResourceList };
readonly libraryInitializers?: ResourceList,
readonly extensions?: BootJsonDataExtension
}
export type ResourceList = { [name: string]: string };
......
......@@ -24,4 +24,4 @@ export interface WebAssemblyStartOptions {
// This type doesn't have to align with anything in BootConfig.
// Instead, this represents the public API through which certain aspects
// of boot resource loading can be customized.
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization';
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization' | 'manifest';
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册