使用 ISpaBuilder.UseReactDevelopmentServer 时注入服务器端数据
Injecting serverside data when using ISpaBuilder.UseReactDevelopmentServer
当使用 ASP.NET(核心、.NET 5)MVC 的 IApplicationBuilder.UseSpa
/ ISpaBuilder.UseReactDevelopmentServer
(开发中)时,有没有办法在之前对索引 HTML 进行后处理它被发送到浏览器?我需要注入一个脚本标签,其中包含有关当前已授权用户的数据,以供 React 应用程序使用。
我想避免为了在启动时获取当前登录的用户而不得不从我的 React 应用程序内部进行额外调用。
您可以使用自定义中间件来做到这一点。假设您使用的是 .net core 3.1,中间件看起来应该是这样的:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace TestReactDevServer.Middleware
{
public class ScriptInjectorMiddleware
{
private readonly RequestDelegate _next;
public ScriptInjectorMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
//Save pointer to the original response body stream
var originalBodyStream = context.Response.Body;
//Create new memory stream
using (var responseBody = new MemoryStream())
{
//...and use it for subsequent requests so we can peek the contents
context.Response.Body = responseBody;
//Continue down the Middleware pipeline, eventually returning to this class
await _next(context);
//inspect response, inject script
await InjectScript(responseBody, "window.myUser='123';");
//copy the contents of the new memory stream to the original place
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<Stream> InjectScript(Stream input, string script)
{
input.Seek(0, SeekOrigin.Begin);
var decompressed = new MemoryStream();
using (var tmp = new GZipStream(input, CompressionMode.Decompress, true))
{
tmp.CopyTo(decompressed);
}
var html = await decompressed.StreamToString();
var modifiedHtml = Regex.Replace(html, "</body>[\n\r]+</html>", $"<script type=\"text/javascript\">{script}</script></body></html>", RegexOptions.IgnoreCase | RegexOptions.Multiline); // any way to locate closing tags will work here, you probably can be more efficient
input.Seek(0, SeekOrigin.Begin);
using (var modifiedHtmlStream = modifiedHtml.ToStream())
using (var tmp = new GZipStream(input, CompressionMode.Compress, true)) // might be optional
{
modifiedHtmlStream.CopyTo(tmp);
}
return input;
}
}
public static class ScriptInjectorMiddlewareExtensions
{
public static IApplicationBuilder UseScriptInjectorMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ScriptInjectorMiddleware>();
}
}
public static class StreamExtensions {
public static async Task<string> StreamToString(this Stream stream) {
stream.Seek(0, SeekOrigin.Begin);
return await new StreamReader(stream).ReadToEndAsync();
}
public static Stream ToStream(this string str)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(str);
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
}
有几点需要指出:
- 使用 Chrome 对此进行测试,我最终不得不解压缩代理响应并在修改后将其压缩回来 - 我认为压缩步骤可能是可选的。
- 根据您的用户代理,您可能需要处理更多压缩情况(请参阅 Github 上的更多示例)
- 您需要在
Startup.cs
中调用 .UseSpa()
之前注入此中间件:添加 app.UseUserInjectorMiddleware();
应该选择包含的扩展方法
我怀疑这个例子还远未完成,尤其是在处理不同的编码和内容类型方面 - 我希望您能够根据您的用例调整这个想法。
当使用 ASP.NET(核心、.NET 5)MVC 的 IApplicationBuilder.UseSpa
/ ISpaBuilder.UseReactDevelopmentServer
(开发中)时,有没有办法在之前对索引 HTML 进行后处理它被发送到浏览器?我需要注入一个脚本标签,其中包含有关当前已授权用户的数据,以供 React 应用程序使用。
我想避免为了在启动时获取当前登录的用户而不得不从我的 React 应用程序内部进行额外调用。
您可以使用自定义中间件来做到这一点。假设您使用的是 .net core 3.1,中间件看起来应该是这样的:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace TestReactDevServer.Middleware
{
public class ScriptInjectorMiddleware
{
private readonly RequestDelegate _next;
public ScriptInjectorMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
//Save pointer to the original response body stream
var originalBodyStream = context.Response.Body;
//Create new memory stream
using (var responseBody = new MemoryStream())
{
//...and use it for subsequent requests so we can peek the contents
context.Response.Body = responseBody;
//Continue down the Middleware pipeline, eventually returning to this class
await _next(context);
//inspect response, inject script
await InjectScript(responseBody, "window.myUser='123';");
//copy the contents of the new memory stream to the original place
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<Stream> InjectScript(Stream input, string script)
{
input.Seek(0, SeekOrigin.Begin);
var decompressed = new MemoryStream();
using (var tmp = new GZipStream(input, CompressionMode.Decompress, true))
{
tmp.CopyTo(decompressed);
}
var html = await decompressed.StreamToString();
var modifiedHtml = Regex.Replace(html, "</body>[\n\r]+</html>", $"<script type=\"text/javascript\">{script}</script></body></html>", RegexOptions.IgnoreCase | RegexOptions.Multiline); // any way to locate closing tags will work here, you probably can be more efficient
input.Seek(0, SeekOrigin.Begin);
using (var modifiedHtmlStream = modifiedHtml.ToStream())
using (var tmp = new GZipStream(input, CompressionMode.Compress, true)) // might be optional
{
modifiedHtmlStream.CopyTo(tmp);
}
return input;
}
}
public static class ScriptInjectorMiddlewareExtensions
{
public static IApplicationBuilder UseScriptInjectorMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ScriptInjectorMiddleware>();
}
}
public static class StreamExtensions {
public static async Task<string> StreamToString(this Stream stream) {
stream.Seek(0, SeekOrigin.Begin);
return await new StreamReader(stream).ReadToEndAsync();
}
public static Stream ToStream(this string str)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(str);
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
}
有几点需要指出:
- 使用 Chrome 对此进行测试,我最终不得不解压缩代理响应并在修改后将其压缩回来 - 我认为压缩步骤可能是可选的。
- 根据您的用户代理,您可能需要处理更多压缩情况(请参阅 Github 上的更多示例)
- 您需要在
Startup.cs
中调用.UseSpa()
之前注入此中间件:添加app.UseUserInjectorMiddleware();
应该选择包含的扩展方法
我怀疑这个例子还远未完成,尤其是在处理不同的编码和内容类型方面 - 我希望您能够根据您的用例调整这个想法。