internal static Task <RenderToStringResult> RenderToString( string applicationBasePath, INodeServices nodeServices, CancellationToken applicationStoppingToken, JavaScriptModuleExport bootModule, HttpContext httpContext, object customDataParameter, int timeoutMilliseconds) { // We want to pass the original, unencoded incoming URL data through to Node, so that // server-side code has the same view of the URL as client-side code (on the client, // location.pathname returns an unencoded string). // The following logic handles special characters in URL paths in the same way that // Node and client-side JS does. For example, the path "/a=b%20c" gets passed through // unchanged (whereas other .NET APIs do change it - Path.Value will return it as // "/a=b c" and Path.ToString() will return it as "/a%3db%20c") var requestFeature = httpContext.Features.Get <IHttpRequestFeature>(); var unencodedPathAndQuery = requestFeature.RawTarget; var request = httpContext.Request; var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}"; return(RenderToString( applicationBasePath, nodeServices, applicationStoppingToken, bootModule, unencodedAbsoluteUrl, unencodedPathAndQuery, customDataParameter, timeoutMilliseconds, request.PathBase.ToString())); }
public static Task <RenderToStringResult> RenderToString( string applicationBasePath, INodeServices nodeServices, CancellationToken applicationStoppingToken, JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery, object customDataParameter, int timeoutMilliseconds, string requestPathBase) { return(nodeServices.InvokeExportAsync <RenderToStringResult>( GetNodeScriptFilename(applicationStoppingToken), "renderToString", applicationBasePath, bootModule, requestAbsoluteUrl, requestPathAndQuery, customDataParameter, timeoutMilliseconds, requestPathBase)); }
/// <summary> /// Enables server-side prerendering middleware for a Single Page Application. /// </summary> /// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param> /// <param name="configuration">Supplies configuration for the prerendering middleware.</param> public static IApplicationBuilder UseSpaPrerendering( this ISpaBuilder spaBuilder, Action <MintPlayer.AspNetCore.Builder.SpaPrerenderingOptions> configuration) { if (spaBuilder == null) { throw new ArgumentNullException(nameof(spaBuilder)); } if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } var options = new Builder.SpaPrerenderingOptions(); configuration.Invoke(options); var capturedBootModulePath = options.BootModulePath; if (string.IsNullOrEmpty(capturedBootModulePath)) { throw new InvalidOperationException($"To use {nameof(UseSpaPrerendering)}, you " + $"must set a nonempty value on the ${nameof(SpaPrerenderingOptions.BootModulePath)} " + $"property on the ${nameof(SpaPrerenderingOptions)}."); } // If we're building on demand, start that process in the background now var buildOnDemandTask = options.BootModuleBuilder?.Build(spaBuilder); // Get all the necessary context info that will be used for each prerendering call var applicationBuilder = spaBuilder.ApplicationBuilder; var serviceProvider = applicationBuilder.ApplicationServices; var nodeServices = GetNodeServices(serviceProvider); var applicationStoppingToken = serviceProvider.GetRequiredService <IHostApplicationLifetime>() .ApplicationStopping; var applicationBasePath = serviceProvider.GetRequiredService <IWebHostEnvironment>() .ContentRootPath; var moduleExport = new JavaScriptModuleExport(capturedBootModulePath); var excludePathStrings = (options.ExcludeUrls ?? Array.Empty <string>()) .Select(url => new PathString(url)) .ToArray(); var buildTimeout = spaBuilder.Options.StartupTimeout; applicationBuilder.Use(async(context, next) => { context.Response.OnStarting(async() => { if (options.OnPrepareResponse != null) { await options.OnPrepareResponse(context); } }); // If this URL is excluded, skip prerendering. // This is typically used to ensure that static client-side resources // (e.g., /dist/*.css) are served normally or through SPA development // middleware, and don't return the prerendered index.html page. foreach (var excludePathString in excludePathStrings) { if (context.Request.Path.StartsWithSegments(excludePathString)) { await next(); return; } } // If we're building on demand, wait for that to finish, or raise any build errors if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted) { // For better debuggability, create a per-request timeout that makes it clear if the // prerendering builder took too long for this request, but without aborting the // underlying build task so that subsequent requests could still work. await buildOnDemandTask.WithTimeout(buildTimeout, $"The prerendering build process did not complete within the " + $"timeout period of {buildTimeout.Seconds} seconds. " + $"Check the log output for error information."); } // It's no good if we try to return a 304. We need to capture the actual // HTML content so it can be passed as a template to the prerenderer. RemoveConditionalRequestHeaders(context.Request); // Make sure we're not capturing compressed content, because then we'd have // to decompress it. Since this sub-request isn't leaving the machine, there's // little to no benefit in having compression on it. var originalAcceptEncodingValue = GetAndRemoveAcceptEncodingHeader(context.Request); // Capture the non-prerendered responses, which in production will typically only // be returning the default SPA index.html page (because other resources will be // served statically from disk). We will use this as a template in which to inject // the prerendered output. using (var outputBuffer = new MemoryStream()) { var originalResponseStream = context.Response.Body; context.Response.Body = outputBuffer; try { await next(); outputBuffer.Seek(0, SeekOrigin.Begin); } finally { context.Response.Body = originalResponseStream; if (!string.IsNullOrEmpty(originalAcceptEncodingValue)) { context.Request.Headers[HeaderNames.AcceptEncoding] = originalAcceptEncodingValue; } } // If it isn't an HTML page that we can use as the template for prerendering, // - ... because it's not text/html // - ... or because it's an error // then prerendering doesn't apply to this request, so just pass through the // response as-is. Note that the non-text/html case is not an error: this is // typically how the SPA dev server responses for static content are returned // in development mode. var canPrerender = IsSuccessStatusCode(context.Response.StatusCode) && IsHtmlContentType(context.Response.ContentType); if (!canPrerender) { await outputBuffer.CopyToAsync(context.Response.Body); return; } // Most prerendering logic will want to know about the original, unprerendered // HTML that the client would be getting otherwise. Typically this is used as // a template from which the fully prerendered page can be generated. var customData = new Dictionary <string, object> { { "originalHtml", Encoding.UTF8.GetString(outputBuffer.GetBuffer()) } }; // If the developer wants to use custom logic to pass arbitrary data to the // prerendering JS code (e.g., to pass through cookie data), now's their chance var spaPrerenderingService = context.RequestServices.GetService <Services.ISpaPrerenderingService>(); if (spaPrerenderingService != null) { await spaPrerenderingService.OnSupplyData(context, customData); } var(unencodedAbsoluteUrl, unencodedPathAndQuery) = GetUnencodedUrlAndPathQuery(context); var renderResult = await Prerenderer.RenderToString( applicationBasePath, nodeServices, applicationStoppingToken, moduleExport, unencodedAbsoluteUrl, unencodedPathAndQuery, customDataParameter: customData, timeoutMilliseconds: 0, requestPathBase: context.Request.PathBase.ToString()); await ServePrerenderResult(context, renderResult); } }); return(applicationBuilder); }