noxi雑記

.NET、Angularまわりの小ネタブログ

ASP.NET Core で SPA の HTTP ヘッダーを変更する

ASP.NET Core には Angular や React といった SPA を組み込んだテンプレートがあります。これらのテンプレートを使用した際に、 SPA のファイルを返す時の HTTP ヘッダー付与方法について解説します。

TL;DR

StartupConfigureUseSpaStaticFilesUseSpa の引数にオプションを設定し、 StaticFileOptions#OnPrepareResponse にてレスポンスを操作する。

前提条件

この記事は以下のバージョンで検証しています。また ASP.NET Core のテンプレートに Angular テンプレートを使用しています。

SPA 使用時の ASP.NET Core のミドルウェアの流れ

Angular や React といった SPA のテンプレートを使用して ASP.NET Core のプロジェクトを生成すると、普通の MVC テンプレートと異なるミドルウェアが追加されています。

(Startup.cs)

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    // ↓↓↓↓↓この辺↓↓↓↓↓

    // In production, the Angular files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });

    // ↑↑↑↑↑この辺↑↑↑↑↑
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();

    // ↓↓↓↓↓この辺↓↓↓↓↓

    app.UseSpaStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller}/{action=Index}/{id?}");
    });

    app.UseSpa(spa =>
    {
        // To learn more about options for serving an Angular SPA from ASP.NET Core,
        // see https://go.microsoft.com/fwlink/?linkid=864501

        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });

    // ↑↑↑↑↑この辺↑↑↑↑↑
}

これは何かというと、 SPA を構成するファイル(HTML 、 JS 、 CSS など)の解決をしたり、開発時に Angular CLI のDevサーバーに対してプロキシしたりするための機能です。

SpaStaticFiles

SpaStaticFiles はその名の通り、 SPA の出力ディレクトリ上に存在する JS や CSS などのファイルをレスポンスするための機能です。 StartupUseSpaStaticFiles を呼び出すことで追加されます。
ASP.NET Core 2.1 の Angular/React テンプレートはプロジェクト内の ClientApp ディレクトリに Angular CLI や react-scripts のプロジェクトがそのまま含まれ、パブリッシュ時にこの SPA プロジェクトもビルドして ClientApp の下にビルド結果を保持します。 ASP.NET Core の StaticFilesMiddleware はデフォルトで wwwroot 下のファイルをレスポンスするため、 SpaStaticFiles を使用して wwwroot 下に存在しない SPA のビルド結果をレスポンスします。

Spa

Spa はその名のまま、 SPA を動作させるための機能です。
開発時は自動的に Angular CLI や React の開発サーバーを起動してリクエストをプロキシし、開発時以外では SPA の出力ディレクトリ上の index.html をレスポンスします。

SPA 関連のリクエスト処理ミドルウェア

ここまでを踏まえて、 ASP.NET Core で SPA 利用時にどのミドルウェアがレスポンスを返しているかをまとめてみました。

f:id:noxi515:20181109011831p:plain

f:id:noxi515:20181109011905p:plain

SPA の HTTP ヘッダーを変更する

StaticFiles ミドルウェアには Startup で機能設定時に StaticFilesOptions#PrepareResponse という Action のプロパティを設定しておくことで、静的ファイルレスポンス時の挙動を制御することができます。例えば sample.png というファイルのレスポンスをブラウザにキャッシュさせないようにするには、 Startup#Configure で次の様に設定します。

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = context =>
    {
        // 返却するファイルの名前が sample.png だったら
        if (context.File.Name == "sample.png")
        {
            // HTTPヘッダーのCacheControlとExpiresを設定する
            context.Context.Response.Headers[HeaderNames.CacheControl] =
                $"{CacheControlHeaderValue.NoCacheString},{CacheControlHeaderValue.NoStoreString},{CacheControlHeaderValue.MustRevalidateString}";
            context.Context.Response.Headers[HeaderNames.Expires] = "0";
        }
    }
});

SPA のレスポンスの HTTP ヘッダーを変更するにも同様に、レスポンスするミドルウェアの設定を行います。

// SPA静的ファイル
app.UseSpaStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = context =>
    {
        // 返却するファイルの名前が sample.png だったら
        if (context.File.Name == "sample.png")
        {
            // HTTPヘッダーのCacheControlとExpiresを設定する
            context.Context.Response.Headers[HeaderNames.CacheControl] =
                $"{CacheControlHeaderValue.NoCacheString},{CacheControlHeaderValue.NoStoreString},{CacheControlHeaderValue.MustRevalidateString}";
            context.Context.Response.Headers[HeaderNames.Expires] = "0";
        }
    }
});

// SPA
app.UseSpa(spa =>
{
    // To learn more about options for serving an Angular SPA from ASP.NET Core,
    // see https://go.microsoft.com/fwlink/?linkid=864501

    spa.Options.SourcePath = "ClientApp";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }
    else
    {
        // 開発時以外ではHTTPヘッダーを設定する
        // 開発時はSPAの開発サーバーにプロキシしているだけなので
        spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
        {
            OnPrepareResponse = context =>
            {
                // 返却するファイルの名前が index.html だったら
                if (context.File.Name == "index.html")
                {
                    // HTTPヘッダーのCacheControlとExpiresを設定する
                    context.Context.Response.Headers[HeaderNames.CacheControl] =
                        $"{CacheControlHeaderValue.NoCacheString},{CacheControlHeaderValue.NoStoreString},{CacheControlHeaderValue.MustRevalidateString}";
                    context.Context.Response.Headers[HeaderNames.Expires] = "0";
                }
            }
        };
    }
});