Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7e92413

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

File tree

Expand file treeCollapse file tree

1 file changed

+83
-57
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

1 file changed

+83
-57
lines changed
Open diff view settings
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.AspNetCore.SpaServices.Extensions/Prerendering/SpaPrerenderingExtensions.cs
+83-57Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.