Support overriding how boot resources are loaded. Fixes #5462
This commit is contained in:
parent
fcb96b6edd
commit
e95264f237
File diff suppressed because one or more lines are too long
|
|
@ -9,14 +9,14 @@ import { ConsoleLogger } from './Platform/Logging/Loggers';
|
|||
import { LogLevel, Logger } from './Platform/Logging/Logger';
|
||||
import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
|
||||
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
||||
import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
|
||||
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
|
||||
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
||||
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
|
||||
|
||||
let renderingFailed = false;
|
||||
let started = false;
|
||||
|
||||
async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
||||
async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
|
||||
if (started) {
|
||||
throw new Error('Blazor has already started.');
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
|||
logger.log(LogLevel.Information, 'Blazor server-side application started.');
|
||||
}
|
||||
|
||||
async function initializeConnection(options: BlazorOptions, logger: Logger, circuit: CircuitDescriptor): Promise<signalR.HubConnection> {
|
||||
async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<signalR.HubConnection> {
|
||||
const hubProtocol = new MessagePackHubProtocol();
|
||||
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import { WebAssemblyResourceLoader } from './Platform/WebAssemblyResourceLoader'
|
|||
import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
|
||||
import { BootConfigResult } from './Platform/BootConfig';
|
||||
import { Pointer } from './Platform/Platform';
|
||||
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
|
||||
|
||||
let started = false;
|
||||
|
||||
async function boot(options?: any): Promise<void> {
|
||||
async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
|
||||
|
||||
if (started) {
|
||||
throw new Error('Blazor has already started.');
|
||||
|
|
@ -43,7 +44,7 @@ async function boot(options?: any): Promise<void> {
|
|||
const bootConfigResult = await BootConfigResult.initAsync();
|
||||
|
||||
const [resourceLoader] = await Promise.all([
|
||||
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig),
|
||||
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
|
||||
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { LogLevel } from '../Logging/Logger';
|
||||
|
||||
export interface BlazorOptions {
|
||||
export interface CircuitStartOptions {
|
||||
configureSignalR: (builder: signalR.HubConnectionBuilder) => void;
|
||||
logLevel: LogLevel;
|
||||
reconnectionOptions: ReconnectionOptions;
|
||||
reconnectionHandler?: ReconnectionHandler;
|
||||
}
|
||||
|
||||
export function resolveOptions(userOptions?: Partial<BlazorOptions>): BlazorOptions {
|
||||
export function resolveOptions(userOptions?: Partial<CircuitStartOptions>): CircuitStartOptions {
|
||||
const result = { ...defaultOptions, ...userOptions };
|
||||
|
||||
// The spread operator can't be used for a deep merge, so do the same for subproperties
|
||||
|
|
@ -29,7 +29,7 @@ export interface ReconnectionHandler {
|
|||
onConnectionUp(): void;
|
||||
}
|
||||
|
||||
const defaultOptions: BlazorOptions = {
|
||||
const defaultOptions: CircuitStartOptions = {
|
||||
configureSignalR: (_) => { },
|
||||
logLevel: LogLevel.Warning,
|
||||
reconnectionOptions: {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReconnectionHandler, ReconnectionOptions } from './BlazorOptions';
|
||||
import { ReconnectionHandler, ReconnectionOptions } from './CircuitStartOptions';
|
||||
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
|
||||
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { showErrorNotification } from '../../BootErrors';
|
|||
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader';
|
||||
import { Platform, System_Array, Pointer, System_Object, System_String } from '../Platform';
|
||||
import { loadTimezoneData } from './TimezoneDataFile';
|
||||
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
|
||||
|
||||
let mono_string_get_utf8: (managedString: System_String) => Pointer;
|
||||
let mono_wasm_add_assembly: (name: string, heapAddress: number, length: number) => void;
|
||||
|
|
@ -138,17 +139,30 @@ function addScriptTagsToDocument(resourceLoader: WebAssemblyResourceLoader) {
|
|||
const dotnetJsResourceName = Object
|
||||
.keys(resourceLoader.bootConfig.resources.runtime)
|
||||
.filter(n => n.startsWith('dotnet.') && n.endsWith('.js'))[0];
|
||||
const dotnetJsContentHash = resourceLoader.bootConfig.resources.runtime[dotnetJsResourceName];
|
||||
const scriptElem = document.createElement('script');
|
||||
scriptElem.src = `_framework/wasm/${dotnetJsResourceName}`;
|
||||
scriptElem.defer = true;
|
||||
|
||||
// For consistency with WebAssemblyResourceLoader, we only enforce SRI if caching is allowed
|
||||
if (resourceLoader.bootConfig.cacheBootResources) {
|
||||
const contentHash = resourceLoader.bootConfig.resources.runtime[dotnetJsResourceName];
|
||||
scriptElem.integrity = contentHash;
|
||||
scriptElem.integrity = dotnetJsContentHash;
|
||||
scriptElem.crossOrigin = 'anonymous';
|
||||
}
|
||||
|
||||
// Allow overriding the URI from which the dotnet.*.js file is loaded
|
||||
if (resourceLoader.startOptions.loadBootResource) {
|
||||
const resourceType: WebAssemblyBootResourceType = 'dotnetjs';
|
||||
const customSrc = resourceLoader.startOptions.loadBootResource(
|
||||
resourceType, dotnetJsResourceName, scriptElem.src, dotnetJsContentHash);
|
||||
if (typeof(customSrc) === 'string') {
|
||||
scriptElem.src = customSrc;
|
||||
} else if (customSrc) {
|
||||
// Since we must load this via a <script> tag, it's only valid to supply a URI (and not a Request, say)
|
||||
throw new Error(`For a ${resourceType} resource, custom loaders must supply a URI string.`);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.appendChild(scriptElem);
|
||||
}
|
||||
|
||||
|
|
@ -186,12 +200,13 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
|
||||
// Begin loading the .dll/.pdb/.wasm files, but don't block here. Let other loading processes run in parallel.
|
||||
const dotnetWasmResourceName = 'dotnet.wasm';
|
||||
const assembliesBeingLoaded = resourceLoader.loadResources(resources.assembly, filename => `_framework/_bin/${filename}`);
|
||||
const pdbsBeingLoaded = resourceLoader.loadResources(resources.pdb || {}, filename => `_framework/_bin/${filename}`);
|
||||
const assembliesBeingLoaded = resourceLoader.loadResources(resources.assembly, filename => `_framework/_bin/${filename}`, 'assembly');
|
||||
const pdbsBeingLoaded = resourceLoader.loadResources(resources.pdb || {}, filename => `_framework/_bin/${filename}`, 'pdb');
|
||||
const wasmBeingLoaded = resourceLoader.loadResource(
|
||||
/* name */ dotnetWasmResourceName,
|
||||
/* url */ `_framework/wasm/${dotnetWasmResourceName}`,
|
||||
/* hash */ resourceLoader.bootConfig.resources.runtime[dotnetWasmResourceName]);
|
||||
/* hash */ resourceLoader.bootConfig.resources.runtime[dotnetWasmResourceName],
|
||||
/* type */ 'dotnetwasm');
|
||||
|
||||
const dotnetTimeZoneResourceName = 'dotnet.timezones.dat';
|
||||
let timeZoneResource: LoadingResource | undefined;
|
||||
|
|
@ -199,7 +214,8 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
timeZoneResource = resourceLoader.loadResource(
|
||||
dotnetTimeZoneResourceName,
|
||||
`_framework/wasm/${dotnetTimeZoneResourceName}`,
|
||||
resourceLoader.bootConfig.resources.runtime[dotnetTimeZoneResourceName]);
|
||||
resourceLoader.bootConfig.resources.runtime[dotnetTimeZoneResourceName],
|
||||
'timezonedata');
|
||||
}
|
||||
|
||||
// Override the mechanism for fetching the main wasm file so we can connect it to our cache
|
||||
|
|
@ -243,7 +259,7 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
if (satelliteResources) {
|
||||
const resourcePromises = Promise.all(culturesToLoad
|
||||
.filter(culture => satelliteResources.hasOwnProperty(culture))
|
||||
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/_bin/${fileName}`))
|
||||
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/_bin/${fileName}`, 'assembly'))
|
||||
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
|
||||
.map(async resource => (await resource.response).arrayBuffer()));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { toAbsoluteUri } from '../Services/NavigationManager';
|
||||
import { BootJsonData, ResourceList } from './BootConfig';
|
||||
import { WebAssemblyStartOptions, WebAssemblyBootResourceType } from './WebAssemblyStartOptions';
|
||||
const networkFetchCacheMode = 'no-cache';
|
||||
|
||||
export class WebAssemblyResourceLoader {
|
||||
|
|
@ -7,27 +8,23 @@ export class WebAssemblyResourceLoader {
|
|||
private networkLoads: { [name: string]: LoadLogEntry } = {};
|
||||
private cacheLoads: { [name: string]: LoadLogEntry } = {};
|
||||
|
||||
static async initAsync(bootConfig: BootJsonData): Promise<WebAssemblyResourceLoader> {
|
||||
static async initAsync(bootConfig: BootJsonData, startOptions: Partial<WebAssemblyStartOptions>): Promise<WebAssemblyResourceLoader> {
|
||||
const cache = await getCacheToUseIfEnabled(bootConfig);
|
||||
return new WebAssemblyResourceLoader(bootConfig, cache);
|
||||
return new WebAssemblyResourceLoader(bootConfig, cache, startOptions);
|
||||
}
|
||||
|
||||
constructor(readonly bootConfig: BootJsonData, readonly cacheIfUsed: Cache | null) {
|
||||
constructor(readonly bootConfig: BootJsonData, readonly cacheIfUsed: Cache | null, readonly startOptions: Partial<WebAssemblyStartOptions>) {
|
||||
}
|
||||
|
||||
loadResources(resources: ResourceList, url: (name: string) => string): LoadingResource[] {
|
||||
loadResources(resources: ResourceList, url: (name: string) => string, resourceType: WebAssemblyBootResourceType): LoadingResource[] {
|
||||
return Object.keys(resources)
|
||||
.map(name => this.loadResource(name, url(name), resources[name]));
|
||||
.map(name => this.loadResource(name, url(name), resources[name], resourceType));
|
||||
}
|
||||
|
||||
loadResource(name: string, url: string, contentHash: string): LoadingResource {
|
||||
// Note that if cacheBootResources was explicitly disabled, we also bypass hash checking
|
||||
// This is to give developers an easy opt-out from the entire caching/validation flow if
|
||||
// there's anything they don't like about it.
|
||||
|
||||
loadResource(name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType): LoadingResource {
|
||||
const response = this.cacheIfUsed
|
||||
? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash)
|
||||
: fetch(url, { cache: networkFetchCacheMode, integrity: this.bootConfig.cacheBootResources ? contentHash : undefined });
|
||||
? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash, resourceType)
|
||||
: this.loadResourceWithoutCaching(name, url, contentHash, resourceType);
|
||||
|
||||
return { name, url, response };
|
||||
}
|
||||
|
|
@ -77,7 +74,7 @@ export class WebAssemblyResourceLoader {
|
|||
}
|
||||
}
|
||||
|
||||
private async loadResourceWithCaching(cache: Cache, name: string, url: string, contentHash: string) {
|
||||
private async loadResourceWithCaching(cache: Cache, name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType) {
|
||||
// Since we are going to cache the response, we require there to be a content hash for integrity
|
||||
// checking. We don't want to cache bad responses. There should always be a hash, because the build
|
||||
// process generates this data.
|
||||
|
|
@ -96,12 +93,34 @@ export class WebAssemblyResourceLoader {
|
|||
return cachedResponse;
|
||||
} else {
|
||||
// It's not in the cache. Fetch from network.
|
||||
const networkResponse = await fetch(url, { cache: networkFetchCacheMode, integrity: contentHash });
|
||||
const networkResponse = await this.loadResourceWithoutCaching(name, url, contentHash, resourceType);
|
||||
this.addToCacheAsync(cache, name, cacheKey, networkResponse); // Don't await - add to cache in background
|
||||
return networkResponse;
|
||||
}
|
||||
}
|
||||
|
||||
private loadResourceWithoutCaching(name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType): Promise<Response> {
|
||||
// Allow developers to override how the resource is loaded
|
||||
if (this.startOptions.loadBootResource) {
|
||||
const customLoadResult = this.startOptions.loadBootResource(resourceType, name, url, contentHash);
|
||||
if (customLoadResult instanceof Promise) {
|
||||
// They are supplying an entire custom response, so just use that
|
||||
return customLoadResult;
|
||||
} else if (typeof customLoadResult === 'string') {
|
||||
// They are supplying a custom URL, so use that with the default fetch behavior
|
||||
url = customLoadResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Note that if cacheBootResources was explicitly disabled, we also bypass hash checking
|
||||
// This is to give developers an easy opt-out from the entire caching/validation flow if
|
||||
// there's anything they don't like about it.
|
||||
return fetch(url, {
|
||||
cache: networkFetchCacheMode,
|
||||
integrity: this.bootConfig.cacheBootResources ? contentHash : undefined
|
||||
});
|
||||
}
|
||||
|
||||
private async addToCacheAsync(cache: Cache, name: string, cacheKey: string, response: Response) {
|
||||
// We have to clone in order to put this in the cache *and* not prevent other code from
|
||||
// reading the original response stream.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
export interface WebAssemblyStartOptions {
|
||||
/**
|
||||
* Overrides the built-in boot resource loading mechanism so that boot resources can be fetched
|
||||
* from a custom source, such as an external CDN.
|
||||
* @param type The type of the resource to be loaded.
|
||||
* @param name The name of the resource to be loaded.
|
||||
* @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute.
|
||||
* @param integrity The integrity string representing the expected content in the response.
|
||||
* @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior.
|
||||
*/
|
||||
loadBootResource(type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) : string | Promise<Response> | null | undefined;
|
||||
}
|
||||
|
||||
// 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' | 'timezonedata';
|
||||
|
|
@ -7,6 +7,20 @@
|
|||
<body>
|
||||
<app>Loading...</app>
|
||||
<script src="customJsFileForTests.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||
|
||||
<!--
|
||||
To show we can customize the boot resource loading process, the server looks for these
|
||||
flags when collecting logs, and E2E tests check the right entries were seen.
|
||||
-->
|
||||
<script>
|
||||
Blazor.start({
|
||||
loadBootResource: function (type, name, defaultUri, integrity) {
|
||||
return type === 'dotnetjs'
|
||||
? `${defaultUri}?customizedbootresource=true`
|
||||
: fetch(defaultUri, { integrity: integrity, cache: 'no-cache', headers: { 'customizedbootresource': 'true' } });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace HostedInAspNet.Server
|
||||
{
|
||||
public class RequestLog
|
||||
public class BootResourceRequestLog
|
||||
{
|
||||
private List<string> _requestPaths = new List<string>();
|
||||
|
||||
|
|
@ -14,16 +14,22 @@ namespace HostedInAspNet.Server
|
|||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<RequestLog>();
|
||||
services.AddSingleton<BootResourceRequestLog>();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RequestLog requestLog)
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, BootResourceRequestLog bootResourceRequestLog)
|
||||
{
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
// This is used by E2E tests to verify that the correct resources were fetched
|
||||
requestLog.AddRequest(context.Request);
|
||||
// This is used by E2E tests to verify that the correct resources were fetched,
|
||||
// and that it was possible to override the loading mechanism
|
||||
if (context.Request.Query.ContainsKey("customizedbootresource")
|
||||
|| context.Request.Headers.ContainsKey("customizedbootresource")
|
||||
|| context.Request.Path.Value.EndsWith("/blazor.boot.json"))
|
||||
{
|
||||
bootResourceRequestLog.AddRequest(context.Request);
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var initialResourcesRequested = GetAndClearRequestedPaths();
|
||||
Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/blazor.boot.json")));
|
||||
Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/dotnet.wasm")));
|
||||
Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/dotnet.timezones.dat")));
|
||||
Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith(".js")));
|
||||
Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith(".dll")));
|
||||
|
||||
|
|
@ -57,11 +58,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var subsequentResourcesRequested = GetAndClearRequestedPaths();
|
||||
Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/blazor.boot.json")));
|
||||
Assert.Empty(subsequentResourcesRequested.Where(path => path.EndsWith("/dotnet.wasm")));
|
||||
Assert.Empty(subsequentResourcesRequested.Where(path => path.EndsWith("/dotnet.timezones.dat")));
|
||||
Assert.NotEmpty(subsequentResourcesRequested.Where(path => path.EndsWith(".js")));
|
||||
Assert.Empty(subsequentResourcesRequested.Where(path => path.EndsWith(".dll")));
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/20154")]
|
||||
[Fact]
|
||||
public void IncrementallyUpdatesCache()
|
||||
{
|
||||
// Perform a first load to populate the cache
|
||||
|
|
@ -137,7 +139,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
|
||||
private IReadOnlyCollection<string> GetAndClearRequestedPaths()
|
||||
{
|
||||
var requestLog = _serverFixture.Host.Services.GetRequiredService<RequestLog>();
|
||||
var requestLog = _serverFixture.Host.Services.GetRequiredService<BootResourceRequestLog>();
|
||||
var result = requestLog.RequestPaths.ToList();
|
||||
requestLog.Clear();
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Reference in New Issue