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

Commit 7e92413

Browse files
Make UseSpaPrerendering capture the non-prerendered response and supply it to the boot function
1 parent b2c1062 commit 7e92413

File tree

1 file changed

+83
-57
lines changed

1 file changed

+83
-57
lines changed

src/Microsoft.AspNetCore.SpaServices.Extensions/Prerendering/SpaPrerenderingExtensions.cs

Lines changed: 83 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
using Microsoft.AspNetCore.Http.Extensions;
77
using Microsoft.AspNetCore.Http.Features;
88
using Microsoft.AspNetCore.NodeServices;
9-
using Microsoft.AspNetCore.SpaServices;
109
using Microsoft.AspNetCore.SpaServices.Prerendering;
1110
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Net.Http.Headers;
1212
using System;
13+
using System.Collections.Generic;
14+
using System.IO;
15+
using System.Linq;
16+
using System.Text;
1317
using System.Threading.Tasks;
1418

1519
namespace Microsoft.AspNetCore.Builder
@@ -25,33 +29,18 @@ public static class SpaPrerenderingExtensions
2529
/// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
2630
/// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param>
2731
/// <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>
32+
/// <param name="excludeUrls">Optional. If specified, requests within these URL paths will bypass the prerenderer.</param>
2833
public static void UseSpaPrerendering(
2934
this IApplicationBuilder appBuilder,
3035
string entryPoint,
31-
ISpaPrerendererBuilder buildOnDemand = null)
36+
ISpaPrerendererBuilder buildOnDemand = null,
37+
string[] excludeUrls = null)
3238
{
3339
if (string.IsNullOrEmpty(entryPoint))
3440
{
3541
throw new ArgumentException("Cannot be null or empty", nameof(entryPoint));
3642
}
3743

38-
var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(appBuilder);
39-
if (defaultPageMiddleware == null)
40-
{
41-
throw new Exception($"{nameof(UseSpaPrerendering)} should be called inside the 'configure' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
42-
}
43-
44-
var urlPrefix = defaultPageMiddleware.UrlPrefix;
45-
if (urlPrefix == null || urlPrefix.Length < 2)
46-
{
47-
throw new ArgumentException(
48-
"If you are using server-side prerendering, the SPA's public path must be " +
49-
"set to a non-empty and non-root value. This makes it possible to identify " +
50-
"requests for the SPA's internal static resources, so the prerenderer knows " +
51-
"not to return prerendered HTML for those requests.",
52-
nameof(urlPrefix));
53-
}
54-
5544
// We only want to start one build-on-demand task, but it can't commence until
5645
// a request comes in (because we need to wait for all middleware to be configured)
5746
var lazyBuildOnDemandTask = new Lazy<Task>(() => buildOnDemand?.Build(appBuilder));
@@ -64,54 +53,89 @@ public static void UseSpaPrerendering(
6453
var applicationBasePath = serviceProvider.GetRequiredService<IHostingEnvironment>()
6554
.ContentRootPath;
6655
var moduleExport = new JavaScriptModuleExport(entryPoint);
67-
var urlPrefixAsPathString = new PathString(urlPrefix);
68-
69-
// Add the actual middleware that intercepts requests for the SPA default file
70-
// and invokes the prerendering code
56+
var excludePathStrings = (excludeUrls ?? Array.Empty<string>())
57+
.Select(url => new PathString(url))
58+
.ToArray();
59+
60+
// Capture the non-prerendered responses, which in production will typically only
61+
// be returning the default SPA index.html page (because other resources will be
62+
// served statically from disk). We will use this as a template in which to inject
63+
// the prerendered output.
7164
appBuilder.Use(async (context, next) =>
7265
{
73-
// Don't interfere with requests that are within the SPA's urlPrefix, because
74-
// these requests are meant to serve its internal resources (.js, .css, etc.)
75-
if (context.Request.Path.StartsWithSegments(urlPrefixAsPathString))
66+
// If this URL is excluded, skip prerendering
67+
foreach (var excludePathString in excludePathStrings)
7668
{
77-
await next();
78-
return;
69+
if (context.Request.Path.StartsWithSegments(excludePathString))
70+
{
71+
await next();
72+
return;
73+
}
7974
}
8075

81-
// If we're building on demand, do that first
82-
var buildOnDemandTask = lazyBuildOnDemandTask.Value;
83-
if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted)
84-
{
85-
await buildOnDemandTask;
86-
}
76+
// It's no good if we try to return a 304. We need to capture the actual
77+
// HTML content so it can be passed as a template to the prerenderer.
78+
RemoveConditionalRequestHeaders(context.Request);
8779

88-
// As a workaround for @angular/cli not emitting the index.html in 'server'
89-
// builds, pass through a URL that can be used for obtaining it. Longer term,
90-
// remove this.
91-
var customData = new
80+
using (var outputBuffer = new MemoryStream())
9281
{
93-
templateUrl = GetDefaultFileAbsoluteUrl(context, defaultPageMiddleware.DefaultPageUrl)
94-
};
95-
96-
// TODO: Add an optional "supplyCustomData" callback param so people using
97-
// UsePrerendering() can, for example, pass through cookies into the .ts code
98-
99-
var (unencodedAbsoluteUrl, unencodedPathAndQuery) = GetUnencodedUrlAndPathQuery(context);
100-
var renderResult = await Prerenderer.RenderToString(
101-
applicationBasePath,
102-
nodeServices,
103-
applicationStoppingToken,
104-
moduleExport,
105-
unencodedAbsoluteUrl,
106-
unencodedPathAndQuery,
107-
customDataParameter: customData,
108-
timeoutMilliseconds: 0,
109-
requestPathBase: context.Request.PathBase.ToString());
110-
111-
await ApplyRenderResult(context, renderResult);
82+
var originalResponseStream = context.Response.Body;
83+
context.Response.Body = outputBuffer;
84+
85+
try
86+
{
87+
await next();
88+
outputBuffer.Seek(0, SeekOrigin.Begin);
89+
}
90+
finally
91+
{
92+
context.Response.Body = originalResponseStream;
93+
}
94+
95+
// If we're building on demand, do that first
96+
var buildOnDemandTask = lazyBuildOnDemandTask.Value;
97+
if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted)
98+
{
99+
await buildOnDemandTask;
100+
}
101+
102+
// Most prerendering logic will want to know about the original, unprerendered
103+
// HTML that the client would be getting otherwise. Typically this is used as
104+
// a template from which the fully prerendered page can be generated.
105+
var customData = new Dictionary<string, object>
106+
{
107+
{ "originalHtml", Encoding.UTF8.GetString(outputBuffer.GetBuffer()) }
108+
};
109+
110+
// TODO: Add an optional "supplyCustomData" callback param so people using
111+
// UsePrerendering() can, for example, pass through cookies into the .ts code
112+
113+
var (unencodedAbsoluteUrl, unencodedPathAndQuery) = GetUnencodedUrlAndPathQuery(context);
114+
var renderResult = await Prerenderer.RenderToString(
115+
applicationBasePath,
116+
nodeServices,
117+
applicationStoppingToken,
118+
moduleExport,
119+
unencodedAbsoluteUrl,
120+
unencodedPathAndQuery,
121+
customDataParameter: customData,
122+
timeoutMilliseconds: 0,
123+
requestPathBase: context.Request.PathBase.ToString());
124+
125+
await ApplyRenderResult(context, renderResult);
126+
}
112127
});
113128
}
114129

130+
private static void RemoveConditionalRequestHeaders(HttpRequest request)
131+
{
132+
request.Headers.Remove(HeaderNames.IfMatch);
133+
request.Headers.Remove(HeaderNames.IfModifiedSince);
134+
request.Headers.Remove(HeaderNames.IfNoneMatch);
135+
request.Headers.Remove(HeaderNames.IfUnmodifiedSince);
136+
request.Headers.Remove(HeaderNames.IfRange);
137+
}
138+
115139
private static (string, string) GetUnencodedUrlAndPathQuery(HttpContext httpContext)
116140
{
117141
// This is a duplicate of code from Prerenderer.cs in the SpaServices package.
@@ -128,6 +152,8 @@ private static (string, string) GetUnencodedUrlAndPathQuery(HttpContext httpCont
128152

129153
private static async Task ApplyRenderResult(HttpContext context, RenderToStringResult renderResult)
130154
{
155+
context.Response.Clear();
156+
131157
if (!string.IsNullOrEmpty(renderResult.RedirectUrl))
132158
{
133159
context.Response.Redirect(renderResult.RedirectUrl);

0 commit comments

Comments
 (0)