public static async Task <RenderToStringResult> Prerender(this HttpRequest trans) { // Grab key website data that will be serialized for use in universal rendering INodeServices node = trans.HttpContext.RequestServices.GetRequiredService <INodeServices>( ); IHostingEnvironment zone = trans.HttpContext.RequestServices.GetRequiredService <IHostingEnvironment>( ); IHttpRequestFeature item = trans.HttpContext.Features.Get <IHttpRequestFeature>( ); // Identify web app's host and subroute to target for prerending server-side string root = zone.ContentRootPath; string path = item.RawTarget; // Build out the url from the previous parameters for prerendering it's content string url = $"{ trans.Scheme }://{ trans.Host }{ path }"; // Allow for the passing of custom data as a request for the frontend app TransferData data = new TransferData( ); // Customized data sent to the frontend is sent through here as a parameter // Feel free to add more custom data here through TransferData class fields data.elements = trans.Decode( ); data.thisCameFromDotNET = "The server beckons thee!!!"; // Requires a cancellation token for performing universal app prerendering CancellationTokenSource origin = new CancellationTokenSource( ); CancellationToken exe = origin.Token; // Locate the generated server-side bundle used for the initial prerendering JavaScriptModuleExport js = new JavaScriptModuleExport(root + "/Node/server.bundle"); // Serialize and prerender the frontend app as a universal/isomorphic one return(await Prerenderer.RenderToString("/", node, exe, js, url, path, data, 30000, trans.PathBase.ToString( ))); }
public SongsController() { path = AppDomain.CurrentDomain.BaseDirectory; module = new JavaScriptModuleExport("Scripts\\angular\\dist\\server"); var services = new ServiceCollection(); services.AddNodeServices(options => { options.ProjectPath = AppDomain.CurrentDomain.BaseDirectory; }); var serviceProvider = services.BuildServiceProvider(); nodeServices = serviceProvider.GetRequiredService <INodeServices>(); }
/// <summary> /// Enables server-side prerendering middleware for a Single Page Application. /// </summary> /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param> /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param> /// <param name="buildOnDemand">Optional. If specified, executes the supplied <see cref="ISpaPrerendererBuilder"/> before looking for the <paramref name="entryPoint"/> file. This is only intended to be used during development.</param> public static void UseSpaPrerendering( this IApplicationBuilder appBuilder, string entryPoint, ISpaPrerendererBuilder buildOnDemand = null) { if (string.IsNullOrEmpty(entryPoint)) { throw new ArgumentException("Cannot be null or empty", nameof(entryPoint)); } var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(appBuilder); if (defaultPageMiddleware == null) { throw new Exception($"{nameof(UseSpaPrerendering)} should be called inside the 'configure' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}."); } var urlPrefix = defaultPageMiddleware.UrlPrefix; if (urlPrefix == null || urlPrefix.Length < 2) { throw new ArgumentException( "If you are using server-side prerendering, the SPA's public path must be " + "set to a non-empty and non-root value. This makes it possible to identify " + "requests for the SPA's internal static resources, so the prerenderer knows " + "not to return prerendered HTML for those requests.", nameof(urlPrefix)); } // We only want to start one build-on-demand task, but it can't commence until // a request comes in (because we need to wait for all middleware to be configured) var lazyBuildOnDemandTask = new Lazy <Task>(() => buildOnDemand?.Build(appBuilder)); // Get all the necessary context info that will be used for each prerendering call var serviceProvider = appBuilder.ApplicationServices; var nodeServices = GetNodeServices(serviceProvider); var applicationStoppingToken = serviceProvider.GetRequiredService <IApplicationLifetime>() .ApplicationStopping; var applicationBasePath = serviceProvider.GetRequiredService <IHostingEnvironment>() .ContentRootPath; var moduleExport = new JavaScriptModuleExport(entryPoint); var urlPrefixAsPathString = new PathString(urlPrefix); // Add the actual middleware that intercepts requests for the SPA default file // and invokes the prerendering code appBuilder.Use(async(context, next) => { // Don't interfere with requests that are within the SPA's urlPrefix, because // these requests are meant to serve its internal resources (.js, .css, etc.) if (context.Request.Path.StartsWithSegments(urlPrefixAsPathString)) { await next(); return; } // If we're building on demand, do that first var buildOnDemandTask = lazyBuildOnDemandTask.Value; if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted) { await buildOnDemandTask; } // As a workaround for @angular/cli not emitting the index.html in 'server' // builds, pass through a URL that can be used for obtaining it. Longer term, // remove this. var customData = new { templateUrl = GetDefaultFileAbsoluteUrl(context, defaultPageMiddleware.DefaultPageUrl) }; // TODO: Add an optional "supplyCustomData" callback param so people using // UsePrerendering() can, for example, pass through cookies into the .ts code var renderResult = await Prerenderer.RenderToString( applicationBasePath, nodeServices, applicationStoppingToken, moduleExport, context, customDataParameter: customData, timeoutMilliseconds: 0); await ApplyRenderResult(context, renderResult); }); }
/// <summary> /// Adds middleware for server-side prerendering of a Single Page Application. /// </summary> /// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param> /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param> /// <param name="buildOnDemand">Optional. If specified, executes the supplied <see cref="ISpaPrerendererBuilder"/> before looking for the <paramref name="entryPoint"/> file. This is only intended to be used during development.</param> public static void UsePrerendering( this ISpaBuilder spaBuilder, string entryPoint, ISpaPrerendererBuilder buildOnDemand = null) { if (string.IsNullOrEmpty(entryPoint)) { throw new ArgumentException("Cannot be null or empty", nameof(entryPoint)); } // We only want to start one build-on-demand task, but it can't commence until // a request comes in (because we need to wait for all middleware to be configured) var lazyBuildOnDemandTask = new Lazy <Task>(() => buildOnDemand?.Build(spaBuilder)); // Get all the necessary context info that will be used for each prerendering call var appBuilder = spaBuilder.AppBuilder; var serviceProvider = appBuilder.ApplicationServices; var nodeServices = GetNodeServices(serviceProvider); var applicationStoppingToken = GetRequiredService <IApplicationLifetime>(serviceProvider) .ApplicationStopping; var applicationBasePath = GetRequiredService <IHostingEnvironment>(serviceProvider) .ContentRootPath; var moduleExport = new JavaScriptModuleExport(entryPoint); // Add the actual middleware that intercepts requests for the SPA default file // and invokes the prerendering code appBuilder.Use(async(context, next) => { // Don't interfere with requests that aren't meant to render the SPA default file if (!context.Items.ContainsKey(SpaExtensions.IsSpaFallbackRequestTag)) { await next(); return; } // If we're building on demand, do that first var buildOnDemandTask = lazyBuildOnDemandTask.Value; if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted) { await buildOnDemandTask; } // If we're waiting for other SPA initialization tasks, do that first. await spaBuilder.StartupTasks; // As a workaround for @angular/cli not emitting the index.html in 'server' // builds, pass through a URL that can be used for obtaining it. Longer term, // remove this. var customData = new { templateUrl = GetDefaultFileAbsoluteUrl(spaBuilder, context) }; // TODO: Add an optional "supplyCustomData" callback param so people using // UsePrerendering() can, for example, pass through cookies into the .ts code var renderResult = await Prerenderer.RenderToString( applicationBasePath, nodeServices, applicationStoppingToken, moduleExport, context, customDataParameter: customData, timeoutMilliseconds: 0); await ApplyRenderResult(context, renderResult); }); }
public static PrerenderResult Prerender(this ControllerBase controller, JavaScriptModuleExport exportToPrerender, object dataToSupply = null) { return(new PrerenderResult(exportToPrerender, dataToSupply)); }
public PrerenderResult(JavaScriptModuleExport moduleExport, object dataToSupply = null) { _moduleExport = moduleExport; _dataToSupply = dataToSupply; }
/// <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 void UseSpaPrerendering( this ISpaBuilder spaBuilder, Action <SpaPrerenderingOptions> configuration) { if (spaBuilder == null) { throw new ArgumentNullException(nameof(spaBuilder)); } if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } var options = new 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 <IApplicationLifetime>() .ApplicationStopping; var applicationBasePath = serviceProvider.GetRequiredService <IHostingEnvironment>() .ContentRootPath; var moduleExport = new JavaScriptModuleExport(capturedBootModulePath); var excludePathStrings = (options.ExcludeUrls ?? Array.Empty <string>()) .Select(url => new PathString(url)) .ToArray(); applicationBuilder.Use(async(context, next) => { // 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) { await buildOnDemandTask; } // 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 options.SupplyData?.Invoke(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); } }); }
/// <summary> /// Enables server-side prerendering middleware for a Single Page Application. /// </summary> /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param> /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param> /// <param name="buildOnDemand">Optional. If specified, executes the supplied <see cref="ISpaPrerendererBuilder"/> before looking for the <paramref name="entryPoint"/> file. This is only intended to be used during development.</param> /// <param name="excludeUrls">Optional. If specified, requests within these URL paths will bypass the prerenderer.</param> /// <param name="supplyData">Optional. If specified, this callback will be invoked during prerendering, allowing you to pass additional data to the prerendering entrypoint code.</param> public static void UseSpaPrerendering( this IApplicationBuilder appBuilder, string entryPoint, ISpaPrerendererBuilder buildOnDemand = null, string[] excludeUrls = null, Action <HttpContext, IDictionary <string, object> > supplyData = null) { if (string.IsNullOrEmpty(entryPoint)) { throw new ArgumentException("Cannot be null or empty", nameof(entryPoint)); } // If we're building on demand, start that process now var buildOnDemandTask = buildOnDemand?.Build(appBuilder); // Get all the necessary context info that will be used for each prerendering call var serviceProvider = appBuilder.ApplicationServices; var nodeServices = GetNodeServices(serviceProvider); var applicationStoppingToken = serviceProvider.GetRequiredService <IApplicationLifetime>() .ApplicationStopping; var applicationBasePath = serviceProvider.GetRequiredService <IHostingEnvironment>() .ContentRootPath; var moduleExport = new JavaScriptModuleExport(entryPoint); var excludePathStrings = (excludeUrls ?? Array.Empty <string>()) .Select(url => new PathString(url)) .ToArray(); // 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. appBuilder.Use(async(context, next) => { // If this URL is excluded, skip prerendering foreach (var excludePathString in excludePathStrings) { if (context.Request.Path.StartsWithSegments(excludePathString)) { await next(); return; } } // If we're building on demand, do that first if (buildOnDemandTask != null) { await buildOnDemandTask; } // 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); 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 it's not a success response, we're not going to have any template HTML // to pass to the prerenderer. if (context.Response.StatusCode < 200 || context.Response.StatusCode >= 300) { var message = $"Prerendering failed because no HTML template could be obtained. Check that your SPA is compiling without errors. The {nameof(SpaApplicationBuilderExtensions.UseSpa)}() middleware returned a response with status code {context.Response.StatusCode}"; if (outputBuffer.Length > 0) { message += " and the following content: " + Encoding.UTF8.GetString(outputBuffer.GetBuffer()); } throw new InvalidOperationException(message); } // 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()) } }; supplyData?.Invoke(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 ApplyRenderResult(context, renderResult); } }); }