From 94fc84a9b457839802831544daab1a0da88fbec1 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Mon, 3 Jul 2017 14:07:28 +0100
Subject: [PATCH] Add simpler prerendering API. Fixes #607
---
.../Controllers/HomeController.cs | 15 ++++++
.../Node/prerenderPage.js | 14 ++++++
.../NodeServicesExamples.csproj | 2 +-
samples/misc/NodeServicesExamples/Startup.cs | 1 +
.../Views/Home/Index.cshtml | 1 +
.../Views/Home/Prerendering.cshtml | 21 ++++++++
.../misc/NodeServicesExamples/package.json | 1 +
.../Prerendering/DefaultSpaPrerenderer.cs | 49 +++++++++++++++++++
.../Prerendering/ISpaPrerenderer.cs | 28 +++++++++++
.../Prerendering/PrerenderTagHelper.cs | 37 ++------------
.../Prerendering/Prerenderer.cs | 36 ++++++++++++++
...PrerenderingServiceCollectionExtensions.cs | 27 ++++++++++
.../Prerendering/RenderToStringResult.cs | 26 ++++++++++
13 files changed, 225 insertions(+), 33 deletions(-)
create mode 100644 samples/misc/NodeServicesExamples/Node/prerenderPage.js
create mode 100644 samples/misc/NodeServicesExamples/Views/Home/Prerendering.cshtml
create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/DefaultSpaPrerenderer.cs
create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/ISpaPrerenderer.cs
create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderingServiceCollectionExtensions.cs
diff --git a/samples/misc/NodeServicesExamples/Controllers/HomeController.cs b/samples/misc/NodeServicesExamples/Controllers/HomeController.cs
index 7a4f804..d9f3241 100755
--- a/samples/misc/NodeServicesExamples/Controllers/HomeController.cs
+++ b/samples/misc/NodeServicesExamples/Controllers/HomeController.cs
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.NodeServices;
+using Microsoft.AspNetCore.SpaServices.Prerendering;
namespace NodeServicesExamples.Controllers
{
@@ -34,6 +35,20 @@ namespace NodeServicesExamples.Controllers
return View();
}
+ public async Task Prerendering([FromServices] ISpaPrerenderer prerenderer)
+ {
+ var result = await prerenderer.RenderToString("./Node/prerenderPage");
+
+ if (!string.IsNullOrEmpty(result.RedirectUrl))
+ {
+ return Redirect(result.RedirectUrl);
+ }
+
+ ViewData["PrerenderedHtml"] = result.Html;
+ ViewData["PrerenderedGlobals"] = result.CreateGlobalsAssignmentScript();
+ return View();
+ }
+
public IActionResult Error()
{
return View("~/Views/Shared/Error.cshtml");
diff --git a/samples/misc/NodeServicesExamples/Node/prerenderPage.js b/samples/misc/NodeServicesExamples/Node/prerenderPage.js
new file mode 100644
index 0000000..7912a6f
--- /dev/null
+++ b/samples/misc/NodeServicesExamples/Node/prerenderPage.js
@@ -0,0 +1,14 @@
+var createServerRenderer = require('aspnet-prerendering').createServerRenderer;
+
+module.exports = createServerRenderer(function(params) {
+ return new Promise(function (resolve, reject) {
+ var message = 'The HTML was returned by the prerendering boot function. '
+ + 'The boot function received the following params:'
+ + '' + JSON.stringify(params, null, 4) + '
';
+
+ resolve({
+ html: 'Hello, world!
' + message,
+ globals: { sampleData: { nodeVersion: process.version } }
+ });
+ });
+});
diff --git a/samples/misc/NodeServicesExamples/NodeServicesExamples.csproj b/samples/misc/NodeServicesExamples/NodeServicesExamples.csproj
index 4bbb9ec..eedacab 100644
--- a/samples/misc/NodeServicesExamples/NodeServicesExamples.csproj
+++ b/samples/misc/NodeServicesExamples/NodeServicesExamples.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/samples/misc/NodeServicesExamples/Startup.cs b/samples/misc/NodeServicesExamples/Startup.cs
index 4ef4802..26f2ae8 100755
--- a/samples/misc/NodeServicesExamples/Startup.cs
+++ b/samples/misc/NodeServicesExamples/Startup.cs
@@ -17,6 +17,7 @@ namespace NodeServicesExamples
// Enable Node Services
services.AddNodeServices();
+ services.AddSpaPrerenderer();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
diff --git a/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml b/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml
index 0b13844..bbdc563 100644
--- a/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml
+++ b/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml
@@ -9,4 +9,5 @@
diff --git a/samples/misc/NodeServicesExamples/Views/Home/Prerendering.cshtml b/samples/misc/NodeServicesExamples/Views/Home/Prerendering.cshtml
new file mode 100644
index 0000000..bd5fff9
--- /dev/null
+++ b/samples/misc/NodeServicesExamples/Views/Home/Prerendering.cshtml
@@ -0,0 +1,21 @@
+Server-side prerendering
+
+
+ This sample demonstrates how you can invoke a JavaScript module that contains
+ prerendering logic for a Single-Page Application framework.
+
+
+ Your prerendering boot function will receive parameters that describe the page
+ being rendered and any data supplied by the .NET code. The return value should be
+ a promise that resolves with data to be injected into the page, such as the
+ rendered HTML and any global data that should be made available to client-side code.
+
+
+@Html.Raw(ViewData["PrerenderedHtml"])
+
+
+
+
diff --git a/samples/misc/NodeServicesExamples/package.json b/samples/misc/NodeServicesExamples/package.json
index 58b8a32..9787758 100644
--- a/samples/misc/NodeServicesExamples/package.json
+++ b/samples/misc/NodeServicesExamples/package.json
@@ -2,6 +2,7 @@
"name": "nodeservicesexamples",
"version": "0.0.0",
"dependencies": {
+ "aspnet-prerendering": "^2.0.6",
"babel-core": "^6.7.4",
"babel-preset-es2015": "^6.6.0",
"node-chartist": "^1.0.2"
diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/DefaultSpaPrerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/DefaultSpaPrerenderer.cs
new file mode 100644
index 0000000..946f96d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/DefaultSpaPrerenderer.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ 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 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);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/ISpaPrerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/ISpaPrerenderer.cs
new file mode 100644
index 0000000..183d4ae
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/ISpaPrerenderer.cs
@@ -0,0 +1,28 @@
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.SpaServices.Prerendering
+{
+ ///
+ /// 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.
+ ///
+ public interface ISpaPrerenderer
+ {
+ ///
+ /// 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.
+ ///
+ /// The JavaScript module that exports a prerendering function.
+ /// The name of the export from the JavaScript module, if it is not the default export.
+ /// An optional JSON-serializable object to pass to the JavaScript prerendering function.
+ /// If specified, the prerendering task will time out after this duration if not already completed.
+ ///
+ Task RenderToString(
+ string moduleName,
+ string exportName = null,
+ object customDataParameter = null,
+ int timeoutMilliseconds = default(int));
+ }
+}
diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs
index c2c5308..ffa982e 100644
--- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs
+++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs
@@ -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
/// A representing the operation.
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();
- 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($"");
- }
+ output.PostElement.SetHtmlContent($"");
}
}
}
diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs
index e7ce841..4e2fb16 100644
--- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs
+++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs
@@ -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 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();
+ 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());
+ }
+
///
/// Performs server-side prerendering by invoking code in Node.js.
///
diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderingServiceCollectionExtensions.cs
new file mode 100644
index 0000000..2a6eb0b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderingServiceCollectionExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods for setting up prerendering features in an .
+ ///
+ public static class PrerenderingServiceCollectionExtensions
+ {
+ ///
+ /// Configures the dependency injection system to supply an implementation
+ /// of .
+ ///
+ /// The .
+ public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
+ {
+ serviceCollection.TryAddSingleton();
+ serviceCollection.AddSingleton();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs
index a213baf..1a2e156 100644
--- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs
+++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs
@@ -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.
///
public int? StatusCode { get; set; }
+
+ ///
+ /// Constructs a block of JavaScript code that assigns data from the
+ /// property to the global namespace.
+ ///
+ /// A block of JavaScript code.
+ 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();
+ }
}
}
\ No newline at end of file