StaticFileOptions.OnPrepareResponse 从未调用过 ASP.NET 核心 SPA 网络应用程序

StaticFileOptions.OnPrepareResponse never called for ASP.NET Core SPA web app

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

调查

深入研究,问题似乎是 index.html 的缓存时间超过了应有的时间,根据我在这些 posts:

我在 post and followed that through to . After a bit more digging, I also came across a more comprehensive version of that solution in this Github thread 中遇到了一个和我有类似问题的人,我目前的尝试就是基于此。

类似问题

我发现这个 post 与我的相似:StaticFileOptions.OnPrepareResponse does not get called 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");
        }
    });
}

但是,我发现在我的 SPA 加载时似乎都没有调用 OnPrepareResponse 回调。如果我在任一回调中放置一个断点,它们都不会被击中。如果这只是调试器的问题,我让两个回调都抛出异常,但这对行为也没有影响。 This user 跟帖底部的 GitHub 貌似和我有类似的问题,但是一直没有回复。

最少的代码复制

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

我正在使用 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();
});

当 SPA 尝试检索 index.html 文件(在路径 / 处)时,我可以看到此回调被调用并且服务器的响应包含这些 cache-control headers:

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

更新

查看 SPA 静态文件扩展源代码 here,我似乎观察到了这种行为,因为在使用 React Development Server 提供文件时没有提供静态文件。

我在启用源 link 的情况下进入该代码时确认情况确实如此:

我会试一试,看看是否有办法让 Web 服务器在本地调试应用程序时使用 SPA 静态文件中间件来提供文件。

我已经设法找到一种方法让 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");
        }
    });
}

然后我临时注释掉最后三行:

    //if (Environment.GetEnvironmentVariable("LOCAL_DEVELOPMENT") == "1")
    //{
    //    spa.UseReactDevelopmentServer(npmScript: "start");
    //}

我在 ClientApp 文件夹和 运行 yarn && yarn build.

中打开了一个 PowerShell 控制台

启动 ASP.NET 核心网络应用程序,然后我能够看到索引页面加载了完全禁用缓存的 cache-control headers,这将导致浏览器每次重新加载页面时从服务器请求新的响应:

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

这似乎在生产中工作得很好,但至少这样我能够找到一种方法来确认这些回调在 运行 在本地运行应用程序时可以正常工作,现在我更好地了解为什么以前没有调用这些回调。