StaticFileOptions.OnPrepareResponse 从未调用 ASP.NET Core SPA Web 应用程序

Tag*_*agc 7 c# caching asp.net-core asp-net-core-spa-services

我创建了一个在 ASP.NET Core 5.0 上运行的基于 React 的 SPA,但最近在将重大更改推送到生产环境后遇到了问题 - 用户在尝试访问该网站时经常会看到错误,因为他们的浏览器正在加载过时的内​​容与对后端所做的更改不兼容的 SPA 版本。

调查

深入研究这一点,问题似乎是index.html缓存的时间超过了应有的时间,并且根据我在这些帖子中读到的内容,我应该将其配置为永远不会被缓存:

我在这里的帖子中遇到了一个与我有类似问题的人,并遵循这个解决方案。经过更多挖掘后,我还在这个 Github 线程中发现了该解决方案的更全面版本,并且我当前的尝试基于此。

类似的问题

我发现这篇文章与我的类似:StaticFileOptions.OnPrepareResponse does not get call for index.html。不同之处在于,他们的回调除了他们感兴趣的路径之外的所有内容都被命中,而我的回调根本没有被命中。

当前的解决方案尝试

在我的应用程序中,我将Startup.Configure方法更改为:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseSpaStaticFiles(new StaticFileOptions
    {
        OnPrepareResponse = ctx =>
        {
            if (ctx.Context.Request.Path.StartsWithSegments("/static"))
            {
                // Cache all static resources for 1 year (versioned filenames)
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    Public = true,
                    MaxAge = TimeSpan.FromDays(365),
                };
            }
            else
            {
                // Do not cache explicit `/index.html` or any other files.
                // See also: `DefaultPageStaticFileOptions` below for implicit "/index.html"
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    Public = true,
                    MaxAge = TimeSpan.FromDays(0),
                };
            }
        },
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "spa";
        spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
        {
            OnPrepareResponse = ctx => {
                // Do not cache implicit `/index.html`.  See also: `UseSpaStaticFiles` above
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    Public = true,
                    MaxAge = TimeSpan.FromDays(0),
                };
            },
        };

        if (Environment.GetEnvironmentVariable("LOCAL_DEVELOPMENT") == "1")
        {
            spa.UseReactDevelopmentServer(npmScript: "start");
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

但是,我发现OnPrepareResponse当我的 SPA 加载时,这两个回调似乎都没有被调用。如果我在任一回调中放置断点,则它们都不会被击中。如果这只是调试器的问题,我使两个回调都抛出异常,但这对行为也没有影响。GitHub 帖子底部的这位用户似乎和我有类似的问题,但他从未得到回复。

最少的代码复制

我尝试通过从 Microsoft SPA 模板创建一个新项目并添加与上面相同的代码来最小程度地重现此问题。但是,我仍然没有看到在此代码库中调用任何回调。这些是我设置这个最小代码库所采取的步骤:

  • 打开 PowerShell 窗口
  • 跑步dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
  • 跑步dotnet new reactredux --output temp-spa
  • OnPrepareResponse属性设置app.UseSpaStaticFiles()app.UseSpa()一个简单的回调,我可以在其中放置断点或Debug.WriteLine()语句。

我正在使用 dotnet v6.0.101 并安装了 Node v16.3.0 和 npm v7.15.1。

临时修复

发现有效的方法是在之前的某处添加以下中间件委托app.UseSpa

app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        var requestPath = context.Request.Path.Value!;

        if (requestPath.Equals("/"))
        {
            context.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
            context.Response.Headers.Add("Pragma", "no-cache");
            context.Response.Headers.Add("Expires", "0");
        }

        return Task.FromResult(0);
    });

    await next();
});
Run Code Online (Sandbox Code Playgroud)

当 SPA 尝试检索index.html文件(位于 path /)时,我可以看到此回调被调用,并且服务器的响应包含这些缓存控制标头:

响应缓存控制标头

但是,我更愿意通过以下方式实现此目的SpaStaticFiles,并且我想了解为什么不调用这些回调。

更新

在此处查看 SPA 静态文件扩展源代码,我似乎正在观察此行为,因为使用 React 开发服务器提供文件时未提供静态文件。

在启用源链接的情况下单步执行该代码时,我已经确认了这种情况:步入 SPA 静态文件扩展

我将尝试一下,看看是否有一种方法可以让 Web 服务器在本地调试应用程序时使用 SPA 静态文件中间件来提供文件服务。

Tag*_*agc 3

我设法找到一种方法让OnPrepareResponse回调发挥作用。

我的理解是,这些之前没有被调用,因为我的应用程序被配置为在开发模式下运行时使用 React 开发服务器提供静态文件,而不是从文件系统提供它们。SPA 静态文件扩展被编程为检测这种情况并禁用静态文件服务,如我的问题的更新部分所示。

为了解决这个问题,以便我可以在本地调试时触发这些回调,我禁用了运行 React 开发服务器的代码并单独构建了 SPA,以便它在磁盘上生成静态文件。

具体来说,我将我的Startup.Configure方法更新为以下内容:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseSpaStaticFiles(new StaticFileOptions
    {
        OnPrepareResponse = ctx =>
        {
            // Cache all static resources for 1 year (versioned filenames)
            if (ctx.Context.Request.Path.StartsWithSegments("/static"))
            {
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    Public = true,
                    MaxAge = TimeSpan.FromDays(365),
                };
            }
        },
    });

    app.UseRouting();
    
    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "spa";
        spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
        {
            OnPrepareResponse = ctx => {
                // Do not cache implicit `/index.html`.
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    NoCache = true,
                    NoStore = true,
                    MustRevalidate = true,
                };
            },
        };

        if (Environment.GetEnvironmentVariable("LOCAL_DEVELOPMENT") == "1")
        {
            spa.UseReactDevelopmentServer(npmScript: "start");
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

然后我暂时注释掉了最后三行:

    //if (Environment.GetEnvironmentVariable("LOCAL_DEVELOPMENT") == "1")
    //{
    //    spa.UseReactDevelopmentServer(npmScript: "start");
    //}
Run Code Online (Sandbox Code Playgroud)

我在文件夹中打开 PowerShell 控制台ClientApp并运行yarn && yarn build.

启动 ASP.NET Core Web 应用程序,我可以看到索引页面加载了完全禁用缓存的缓存控制标头,并且会导致浏览器在每次重新加载页面时向服务器请求新的响应:index.html 缓存控制标头

同时,版本化的静态资源被配置为缓存一年,浏览器在页面重新加载时从其内部缓存加载它们:静态资源缓存控制标头

看起来这在生产中效果很好,但至少通过这种方式,我能够找到一种方法来确认这些回调在本地运行应用程序时可以正常工作,现在我更好地了解了为什么这些回调不能正常工作。之前没有被叫过。