Blazor - .NET 9 では .js ファイル名にフィンガープリントが付くので古い .js をキャッシュから import してしまう事故がなくなる件

## .NET 9 から、フィンガープリント付きファイル名で静的アセットにアクセスできるようになった

先日、2024年11月12日にリリースされた .NET 9 からは、Blazor を含む ASP.NET Core Web アプリケーションにおいて、JavaScript ファイルなどの静的アセットの取り扱いが改善されました。その改善のひとつとして、ファイル名に、そのファイルのフィンガープリントが含められるようになりました。

例えば、Blazor Web アプリケーションプロジェクトの `wwwroot` フォルダ内に `helper.js` という JavaScript ファイルを配置したとすると、

```
📂wwwroot
  +- 📜helper.js
```

この `helper.js` ファイルをブラウザ側から取得するには、これまでは `https://.../helper.js` という URL を使用していました。しかしながら、`helper.js` 内のコードが変更されたのちに、再びブラウザでこのファイルを取得する際、諸条件によっては、**ブラウザのキャッシュに残されている変更前の内容がそのまま使用されてしまう**、といったことがあり得ました。

それが .NET 9 以降の ASP.NET Core サーバーでは、`https://.../helper.paeprci3wb.js` といったような、この `helper.js` ファイル内のコンテンツをハッシュ化した値である "フィンガープリント" を含む URL でも取得可能となりました (この例だと、ファイル名の `paeprci3wb` の部分がフィンガープリントになります)。

フィンガープリントは、そのファイルの内容によって決まりますので、ファイルの中身が変わればフィンガープリントも変わるため、つまりはフィンガープリントを含むファイル名が変わります。その結果、**ファイルの変更前後ではファイル名が変わってくる**わけですから、アプリ側が常に**最新のフィンガープリント付きファイル名でそれら JavaScript ファイルを参照**していれば、**キャッシュに残されていた変更前のバージョンを使用してしまう事故がなくなる**わけです。

## フィンガープリント付きのファイル名で、静的アセットを参照するには?

### ソースにフィンガープリント付きファイル名を書くわけにはいかない...

しかしちょっとここで疑問が湧きました。C# コード側で、Blazor の JavaScript 相互運用機能を使用して上記例の `wwwroot/helper.js` ファイルを import したい場合、フィンガープリント付きのファイル名をどうやって指定したらいいのでしょうか。フィンガープリントは、プロジェクトのビルド処理の一環として計算され、前述のとおりファイルの内容が変わればフィンガープリント付きファイル名も変わるので、以下のように、ソースコード上に固定で記述するわけにはいきません。

```js
@inject IJSRuntime JSRuntime
...
@code {
    ...
    protected override async Task OnInitializedAsync()
    {
        if (!this.RendererInfo.IsInteractive) return;

        await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
            // helper.js の内容が変更されたら、ファイル名が "helper.paeprci3wb.js" 
            // ではなくなってしまう!! (そもそもフィンガープリント値がすぐにはわからないし)
            "import", "./helper.paeprci3wb.js");
        ...
    }
}
```

## 新設の Assets プロパティで解決できます、が...

これを回避する方法のひとつとして、.NET 9 から新たに Razor コンポーネントクラスに追加された、**`Assets` プロパティ**を使う方法があります。`Assets` プロパティにインデクサアクセスで静的アセット名を渡すと、フィンガープリント付きのファイル名が返ってきます。つまり、以下のように実装できます。

```js
...
@code {
    ...
    protected override async Task OnInitializedAsync()
    {
        ...
        // Assets プロパティを使って、元のファイル名から、フィンガープリント付きファイル名を取得、
        var helperjs =  this.Assets["helper.js"];

        await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
            // 入手したフィンガープリント付きファイル名で import する
            "import", $"./{helperjs}");
        ...
    }
}
```

## import については元のファイル名のままで OK!

しかし、JavaScript モジュールの import については、実は上記のような `Assets` プロパティを参照は必要ありません。以下のように、**ただ単純に、元のファイル名を指定して import するだけ**でよいです。

```js
...
@code {
    ...
    protected override async Task OnInitializedAsync()
    {
        ...
        await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
            // フィンガープリントなしファイル名で import してしまう!?
            "import", "./helper.js");
        ...
    }
}
```

これだと、フィンガープリント付きファイル名を使っていない以上、昔と変わらないのでは... と思いきや、この状態でこの Blazor Web アプリケーションを実行し、ブラウザの開発者ツールで読み込んだ JavaScript ファイルを確認してみると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/0614fdf2-5f6d-35ce-b4ee-d4b183969318.png)

ちゃんと**フィンガープリント付きのファイル名で参照**していました!

## ブラウザ標準の import map 機能が活躍

この仕掛けは Web ブラウザ標準の import map 機能を利用して実現されています。Blazor Web アプリケーションの `App.razor` ファイルを確認すると、以下のようにマークアップされているのがわかります。

```html:Components/App.razor
<!DOCTYPE html>
<html lang="en">
<head>
    ...
    <ImportMap /> <!-- 👈 ここに注目! -->
    ...
```

この `<ImportMap />` コンポーネントが、JavaScript モジュールの import 時に指定されるオリジナルのファイル名をフィンガープリント付きファイル名にマップする、import map を生成してくれるのです。レンダリング結果を確認すると、以下のようになっています。

```html:このBlazorアプリのレンダリング結果
<!DOCTYPE html>
<html lang="en">
    <head>
        ...

        <!-- 👇以下の type=importmap な script 要素が、
               ImportMap コンポーネントによるレンダリング結果 -->
        <script type="importmap">
        {
            "imports": {
                "./helper.js": "./helper.paeprci3wb.js"
            }
        }</script>

        ...
```

これらのお膳立てのお陰で、開発者は手間なく、フィンガープリント付きファイル名による参照を活用できる次第です。

## おわりに: 諸注意や制約について

JavaScript モジュールを import する以外の場面では、import map の機能によるフィンガープリント付きファイル名への実行マッピングは機能しません。import する以外の場面でフィンガープリント付きファイル名で静的アセットを参照する場合は、先に紹介した `Assets` プロパティを利用するなどの必要があります。

また、今回紹介の ASP.NET Core における静的アセットの取り扱い強化は、自分が調べた限り、何らかサーバー側実装が必要となるようです。すなわち、スタンドアロンな Blazor WebAssembly アプリケーションでは本機能は利用できず、最低限でも ASP.NET Core サーバーでホストする必要があるようです。

あと、これも自分が今回調べた限りにおいては、ですが、Razor コンポーネントに並べて設置する JavaScript ファイル、つまり、`Home.razor` があったとしたら `Home.razor.js` のように、Razor コンポーネントファイル名末尾に `.js` を付けたファイル名で JavaScript ファイルを配置した場合ですが、この場合は、フィンガープリント付きの URL は生成されず、そうなると当然 import map にも出現しないようです。今のところ、`wwwroot` フォルダ以下に配置した場合にのみ、フィンガープリント付きファイル名の生成と import map 対応がされるように見えます。この点はどうぞご注意ください。