mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 01:58:29 +00:00
Add simpler prerendering API. Fixes #607
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of a DI service that provides convenient access to
|
||||
/// server-side prerendering APIs. This is an alternative to prerendering via
|
||||
/// the asp-prerender-module tag helper.
|
||||
/// </summary>
|
||||
internal class DefaultSpaPrerenderer : ISpaPrerenderer
|
||||
{
|
||||
private readonly string _applicationBasePath;
|
||||
private readonly CancellationToken _applicationStoppingToken;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly INodeServices _nodeServices;
|
||||
|
||||
public DefaultSpaPrerenderer(
|
||||
INodeServices nodeServices,
|
||||
IApplicationLifetime applicationLifetime,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_applicationBasePath = hostingEnvironment.ContentRootPath;
|
||||
_applicationStoppingToken = applicationLifetime.ApplicationStopping;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_nodeServices = nodeServices;
|
||||
}
|
||||
|
||||
public Task<RenderToStringResult> RenderToString(
|
||||
string moduleName,
|
||||
string exportName = null,
|
||||
object customDataParameter = null,
|
||||
int timeoutMilliseconds = default(int))
|
||||
{
|
||||
return Prerenderer.RenderToString(
|
||||
_applicationBasePath,
|
||||
_nodeServices,
|
||||
_applicationStoppingToken,
|
||||
new JavaScriptModuleExport(moduleName) { ExportName = exportName },
|
||||
_httpContextAccessor.HttpContext,
|
||||
customDataParameter,
|
||||
timeoutMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a service that can perform server-side prerendering for
|
||||
/// JavaScript-based Single Page Applications. This is an alternative
|
||||
/// to using the 'asp-prerender-module' tag helper.
|
||||
/// </summary>
|
||||
public interface ISpaPrerenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes JavaScript code to perform server-side prerendering for a
|
||||
/// Single-Page Application. This is an alternative to using the
|
||||
/// 'asp-prerender-module' tag helper.
|
||||
/// </summary>
|
||||
/// <param name="moduleName">The JavaScript module that exports a prerendering function.</param>
|
||||
/// <param name="exportName">The name of the export from the JavaScript module, if it is not the default export.</param>
|
||||
/// <param name="customDataParameter">An optional JSON-serializable object to pass to the JavaScript prerendering function.</param>
|
||||
/// <param name="timeoutMilliseconds">If specified, the prerendering task will time out after this duration if not already completed.</param>
|
||||
/// <returns></returns>
|
||||
Task<RenderToStringResult> RenderToString(
|
||||
string moduleName,
|
||||
string exportName = null,
|
||||
object customDataParameter = null,
|
||||
int timeoutMilliseconds = default(int));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
@@ -90,19 +87,6 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
// 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 = ViewContext.HttpContext.Features.Get<IHttpRequestFeature>();
|
||||
var unencodedPathAndQuery = requestFeature.RawTarget;
|
||||
|
||||
var request = ViewContext.HttpContext.Request;
|
||||
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
|
||||
|
||||
var result = await Prerenderer.RenderToString(
|
||||
_applicationBasePath,
|
||||
_nodeServices,
|
||||
@@ -111,11 +95,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
ExportName = ExportName
|
||||
},
|
||||
unencodedAbsoluteUrl,
|
||||
unencodedPathAndQuery,
|
||||
ViewContext.HttpContext,
|
||||
CustomDataParameter,
|
||||
TimeoutMillisecondsParameter,
|
||||
request.PathBase.ToString());
|
||||
TimeoutMillisecondsParameter);
|
||||
|
||||
if (!string.IsNullOrEmpty(result.RedirectUrl))
|
||||
{
|
||||
@@ -134,19 +116,10 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
|
||||
// Also attach any specified globals to the 'window' object. This is useful for transferring
|
||||
// general state between server and client.
|
||||
if (result.Globals != null)
|
||||
var globalsScript = result.CreateGlobalsAssignmentScript();
|
||||
if (!string.IsNullOrEmpty(globalsScript))
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var property in result.Globals.Properties())
|
||||
{
|
||||
stringBuilder.AppendFormat("window.{0} = {1};",
|
||||
property.Name,
|
||||
property.Value.ToString(Formatting.None));
|
||||
}
|
||||
if (stringBuilder.Length > 0)
|
||||
{
|
||||
output.PostElement.SetHtmlContent($"<script>{stringBuilder}</script>");
|
||||
}
|
||||
output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
@@ -14,6 +16,40 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
|
||||
private static StringAsTempFile NodeScript;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs server-side prerendering by invoking code in Node.js.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for setting up prerendering features in an <see cref="IServiceCollection" />.
|
||||
/// </summary>
|
||||
public static class PrerenderingServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the dependency injection system to supply an implementation
|
||||
/// of <see cref="ISpaPrerenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
|
||||
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
@@ -30,5 +32,29 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
/// If set, specifies the HTTP status code that should be sent back with the server response.
|
||||
/// </summary>
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a block of JavaScript code that assigns data from the
|
||||
/// <see cref="Globals"/> property to the global namespace.
|
||||
/// </summary>
|
||||
/// <returns>A block of JavaScript code.</returns>
|
||||
public string CreateGlobalsAssignmentScript()
|
||||
{
|
||||
if (Globals == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
foreach (var property in Globals.Properties())
|
||||
{
|
||||
stringBuilder.AppendFormat("window.{0} = {1};",
|
||||
property.Name,
|
||||
property.Value.ToString(Formatting.None));
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user