Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Commit 94fc84a

Browse files
Add simpler prerendering API. Fixes #607
1 parent 9cce26e commit 94fc84a

13 files changed

+225
-33
lines changed

samples/misc/NodeServicesExamples/Controllers/HomeController.cs

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Threading.Tasks;
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.AspNetCore.NodeServices;
4+
using Microsoft.AspNetCore.SpaServices.Prerendering;
45

56
namespace NodeServicesExamples.Controllers
67
{
@@ -34,6 +35,20 @@ public async Task<IActionResult> Chart([FromServices] INodeServices nodeServices
3435
return View();
3536
}
3637

38+
public async Task<IActionResult> Prerendering([FromServices] ISpaPrerenderer prerenderer)
39+
{
40+
var result = await prerenderer.RenderToString("./Node/prerenderPage");
41+
42+
if (!string.IsNullOrEmpty(result.RedirectUrl))
43+
{
44+
return Redirect(result.RedirectUrl);
45+
}
46+
47+
ViewData["PrerenderedHtml"] = result.Html;
48+
ViewData["PrerenderedGlobals"] = result.CreateGlobalsAssignmentScript();
49+
return View();
50+
}
51+
3752
public IActionResult Error()
3853
{
3954
return View("~/Views/Shared/Error.cshtml");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
var createServerRenderer = require('aspnet-prerendering').createServerRenderer;
2+
3+
module.exports = createServerRenderer(function(params) {
4+
return new Promise(function (resolve, reject) {
5+
var message = 'The HTML was returned by the prerendering boot function. '
6+
+ 'The boot function received the following params:'
7+
+ '<pre>' + JSON.stringify(params, null, 4) + '</pre>';
8+
9+
resolve({
10+
html: '<h3>Hello, world!</h3>' + message,
11+
globals: { sampleData: { nodeVersion: process.version } }
12+
});
13+
});
14+
});

samples/misc/NodeServicesExamples/NodeServicesExamples.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
12+
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.SpaServices\Microsoft.AspNetCore.SpaServices.csproj" />
1313
</ItemGroup>
1414

1515
<ItemGroup>

samples/misc/NodeServicesExamples/Startup.cs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public void ConfigureServices(IServiceCollection services)
1717

1818
// Enable Node Services
1919
services.AddNodeServices();
20+
services.AddSpaPrerenderer();
2021
}
2122

2223
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

samples/misc/NodeServicesExamples/Views/Home/Index.cshtml

+1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
<ul>
1010
<li><a asp-action="ES2015Transpilation">ES2015 transpilation</a></li>
1111
<li><a asp-action="Chart">Server-side chart rendering</a></li>
12+
<li><a asp-action="Prerendering">Server-side SPA prerendering</a></li>
1213
</ul>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<h1>Server-side prerendering</h1>
2+
3+
<p>
4+
This sample demonstrates how you can invoke a JavaScript module that contains
5+
prerendering logic for a Single-Page Application framework.
6+
</p>
7+
</p>
8+
Your prerendering boot function will receive parameters that describe the page
9+
being rendered and any data supplied by the .NET code. The return value should be
10+
a promise that resolves with data to be injected into the page, such as the
11+
rendered HTML and any global data that should be made available to client-side code.
12+
</p>
13+
14+
@Html.Raw(ViewData["PrerenderedHtml"])
15+
16+
<script>@Html.Raw(ViewData["PrerenderedGlobals"])</script>
17+
18+
<script>
19+
// Demonstrates how client-side code can receive data from the prerendering process
20+
console.log('Received Node version from prerendering logic: ' + sampleData.nodeVersion);
21+
</script>

samples/misc/NodeServicesExamples/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "nodeservicesexamples",
33
"version": "0.0.0",
44
"dependencies": {
5+
"aspnet-prerendering": "^2.0.6",
56
"babel-core": "^6.7.4",
67
"babel-preset-es2015": "^6.6.0",
78
"node-chartist": "^1.0.2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Threading;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.NodeServices;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.AspNetCore.SpaServices.Prerendering
8+
{
9+
/// <summary>
10+
/// Default implementation of a DI service that provides convenient access to
11+
/// server-side prerendering APIs. This is an alternative to prerendering via
12+
/// the asp-prerender-module tag helper.
13+
/// </summary>
14+
internal class DefaultSpaPrerenderer : ISpaPrerenderer
15+
{
16+
private readonly string _applicationBasePath;
17+
private readonly CancellationToken _applicationStoppingToken;
18+
private readonly IHttpContextAccessor _httpContextAccessor;
19+
private readonly INodeServices _nodeServices;
20+
21+
public DefaultSpaPrerenderer(
22+
INodeServices nodeServices,
23+
IApplicationLifetime applicationLifetime,
24+
IHostingEnvironment hostingEnvironment,
25+
IHttpContextAccessor httpContextAccessor)
26+
{
27+
_applicationBasePath = hostingEnvironment.ContentRootPath;
28+
_applicationStoppingToken = applicationLifetime.ApplicationStopping;
29+
_httpContextAccessor = httpContextAccessor;
30+
_nodeServices = nodeServices;
31+
}
32+
33+
public Task<RenderToStringResult> RenderToString(
34+
string moduleName,
35+
string exportName = null,
36+
object customDataParameter = null,
37+
int timeoutMilliseconds = default(int))
38+
{
39+
return Prerenderer.RenderToString(
40+
_applicationBasePath,
41+
_nodeServices,
42+
_applicationStoppingToken,
43+
new JavaScriptModuleExport(moduleName) { ExportName = exportName },
44+
_httpContextAccessor.HttpContext,
45+
customDataParameter,
46+
timeoutMilliseconds);
47+
}
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Microsoft.AspNetCore.SpaServices.Prerendering
4+
{
5+
/// <summary>
6+
/// Represents a service that can perform server-side prerendering for
7+
/// JavaScript-based Single Page Applications. This is an alternative
8+
/// to using the 'asp-prerender-module' tag helper.
9+
/// </summary>
10+
public interface ISpaPrerenderer
11+
{
12+
/// <summary>
13+
/// Invokes JavaScript code to perform server-side prerendering for a
14+
/// Single-Page Application. This is an alternative to using the
15+
/// 'asp-prerender-module' tag helper.
16+
/// </summary>
17+
/// <param name="moduleName">The JavaScript module that exports a prerendering function.</param>
18+
/// <param name="exportName">The name of the export from the JavaScript module, if it is not the default export.</param>
19+
/// <param name="customDataParameter">An optional JSON-serializable object to pass to the JavaScript prerendering function.</param>
20+
/// <param name="timeoutMilliseconds">If specified, the prerendering task will time out after this duration if not already completed.</param>
21+
/// <returns></returns>
22+
Task<RenderToStringResult> RenderToString(
23+
string moduleName,
24+
string exportName = null,
25+
object customDataParameter = null,
26+
int timeoutMilliseconds = default(int));
27+
}
28+
}

src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs

+5-32
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
using System;
2-
using System.Text;
32
using System.Threading;
43
using System.Threading.Tasks;
54
using Microsoft.AspNetCore.Hosting;
6-
using Microsoft.AspNetCore.Http.Features;
75
using Microsoft.AspNetCore.Mvc.ViewFeatures;
86
using Microsoft.AspNetCore.Mvc.Rendering;
97
using Microsoft.AspNetCore.NodeServices;
108
using Microsoft.AspNetCore.Razor.TagHelpers;
11-
using Newtonsoft.Json;
129

1310
namespace Microsoft.AspNetCore.SpaServices.Prerendering
1411
{
@@ -90,19 +87,6 @@ public PrerenderTagHelper(IServiceProvider serviceProvider)
9087
/// <returns>A <see cref="Task"/> representing the operation.</returns>
9188
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
9289
{
93-
// We want to pass the original, unencoded incoming URL data through to Node, so that
94-
// server-side code has the same view of the URL as client-side code (on the client,
95-
// location.pathname returns an unencoded string).
96-
// The following logic handles special characters in URL paths in the same way that
97-
// Node and client-side JS does. For example, the path "/a=b%20c" gets passed through
98-
// unchanged (whereas other .NET APIs do change it - Path.Value will return it as
99-
// "/a=b c" and Path.ToString() will return it as "/a%3db%20c")
100-
var requestFeature = ViewContext.HttpContext.Features.Get<IHttpRequestFeature>();
101-
var unencodedPathAndQuery = requestFeature.RawTarget;
102-
103-
var request = ViewContext.HttpContext.Request;
104-
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
105-
10690
var result = await Prerenderer.RenderToString(
10791
_applicationBasePath,
10892
_nodeServices,
@@ -111,11 +95,9 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
11195
{
11296
ExportName = ExportName
11397
},
114-
unencodedAbsoluteUrl,
115-
unencodedPathAndQuery,
98+
ViewContext.HttpContext,
11699
CustomDataParameter,
117-
TimeoutMillisecondsParameter,
118-
request.PathBase.ToString());
100+
TimeoutMillisecondsParameter);
119101

120102
if (!string.IsNullOrEmpty(result.RedirectUrl))
121103
{
@@ -134,19 +116,10 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
134116

135117
// Also attach any specified globals to the 'window' object. This is useful for transferring
136118
// general state between server and client.
137-
if (result.Globals != null)
119+
var globalsScript = result.CreateGlobalsAssignmentScript();
120+
if (!string.IsNullOrEmpty(globalsScript))
138121
{
139-
var stringBuilder = new StringBuilder();
140-
foreach (var property in result.Globals.Properties())
141-
{
142-
stringBuilder.AppendFormat("window.{0} = {1};",
143-
property.Name,
144-
property.Value.ToString(Formatting.None));
145-
}
146-
if (stringBuilder.Length > 0)
147-
{
148-
output.PostElement.SetHtmlContent($"<script>{stringBuilder}</script>");
149-
}
122+
output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
150123
}
151124
}
152125
}

src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs

+36
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Threading;
33
using System.Threading.Tasks;
44
using Microsoft.AspNetCore.NodeServices;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Features;
57

68
namespace Microsoft.AspNetCore.SpaServices.Prerendering
79
{
@@ -14,6 +16,40 @@ public static class Prerenderer
1416

1517
private static StringAsTempFile NodeScript;
1618

19+
internal static Task<RenderToStringResult> RenderToString(
20+
string applicationBasePath,
21+
INodeServices nodeServices,
22+
CancellationToken applicationStoppingToken,
23+
JavaScriptModuleExport bootModule,
24+
HttpContext httpContext,
25+
object customDataParameter,
26+
int timeoutMilliseconds)
27+
{
28+
// We want to pass the original, unencoded incoming URL data through to Node, so that
29+
// server-side code has the same view of the URL as client-side code (on the client,
30+
// location.pathname returns an unencoded string).
31+
// The following logic handles special characters in URL paths in the same way that
32+
// Node and client-side JS does. For example, the path "/a=b%20c" gets passed through
33+
// unchanged (whereas other .NET APIs do change it - Path.Value will return it as
34+
// "/a=b c" and Path.ToString() will return it as "/a%3db%20c")
35+
var requestFeature = httpContext.Features.Get<IHttpRequestFeature>();
36+
var unencodedPathAndQuery = requestFeature.RawTarget;
37+
38+
var request = httpContext.Request;
39+
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
40+
41+
return RenderToString(
42+
applicationBasePath,
43+
nodeServices,
44+
applicationStoppingToken,
45+
bootModule,
46+
unencodedAbsoluteUrl,
47+
unencodedPathAndQuery,
48+
customDataParameter,
49+
timeoutMilliseconds,
50+
request.PathBase.ToString());
51+
}
52+
1753
/// <summary>
1854
/// Performs server-side prerendering by invoking code in Node.js.
1955
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.NodeServices;
6+
using Microsoft.AspNetCore.SpaServices.Prerendering;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
9+
namespace Microsoft.Extensions.DependencyInjection
10+
{
11+
/// <summary>
12+
/// Extension methods for setting up prerendering features in an <see cref="IServiceCollection" />.
13+
/// </summary>
14+
public static class PrerenderingServiceCollectionExtensions
15+
{
16+
/// <summary>
17+
/// Configures the dependency injection system to supply an implementation
18+
/// of <see cref="ISpaPrerenderer"/>.
19+
/// </summary>
20+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
21+
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
22+
{
23+
serviceCollection.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
24+
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
25+
}
26+
}
27+
}

src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs

+26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using Newtonsoft.Json;
12
using Newtonsoft.Json.Linq;
3+
using System.Text;
24

35
namespace Microsoft.AspNetCore.SpaServices.Prerendering
46
{
@@ -30,5 +32,29 @@ public class RenderToStringResult
3032
/// If set, specifies the HTTP status code that should be sent back with the server response.
3133
/// </summary>
3234
public int? StatusCode { get; set; }
35+
36+
/// <summary>
37+
/// Constructs a block of JavaScript code that assigns data from the
38+
/// <see cref="Globals"/> property to the global namespace.
39+
/// </summary>
40+
/// <returns>A block of JavaScript code.</returns>
41+
public string CreateGlobalsAssignmentScript()
42+
{
43+
if (Globals == null)
44+
{
45+
return string.Empty;
46+
}
47+
48+
var stringBuilder = new StringBuilder();
49+
50+
foreach (var property in Globals.Properties())
51+
{
52+
stringBuilder.AppendFormat("window.{0} = {1};",
53+
property.Name,
54+
property.Value.ToString(Formatting.None));
55+
}
56+
57+
return stringBuilder.ToString();
58+
}
3359
}
3460
}

0 commit comments

Comments
 (0)