使用 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;
        }
    }
}

有几点需要指出:

  1. 使用 Chrome 对此进行测试,我最终不得不解压缩代理响应并在修改后将其压缩回来 - 我认为压缩步骤可能是可选的。
  2. 根据您的用户代理,您可能需要处理更多压缩情况(请参阅 Github 上的更多示例)
  3. 您需要在 Startup.cs 中调用 .UseSpa() 之前注入此中间件:添加 app.UseUserInjectorMiddleware(); 应该选择包含的扩展方法

我怀疑这个例子还远未完成,尤其是在处理不同的编码和内容类型方面 - 我希望您能够根据您的用例调整这个想法。