Blazor から JavaScript モジュールを import してグローバルスコープへの露出をなくす

## グローバルスコープへの露出はまだ残されていた

先日、Blazor アドベントカレンダーに以下の投稿がありました。

https://qiita.com/yaju/items/bcca337af393f15a6795

Blazor が備える JavaScript 相互運用機能を使って、Excel ライクなグリッド JavaScript ライブラリ「Handsontable」を Blazor から使ってみる、というテーマでしたね。

自分はこれを少し改良しまして、グローバルスコープへの変数や関数の露出を削減したり、複数グリッドを1ページ内に配置できるようにするなどの変更を施す記事 x 2つを投稿しました (下記)。

https://qiita.com/jsakamoto/items/654f8fbde25c44e54eb9

https://qiita.com/jsakamoto/items/6d820ef3c15e8d51f384

以上のように変更をしてきたのですが、しかし、上記 2 記事を経てもなお、まだ 1 つだけ、JavaScript のグローバルスコープに露出があります。`scripts.js` 内で定義した `createGrid` 関数です (下図参照)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/1f530cc6-6660-4df6-5a96-7e5d6430e82e.png)

Blazor から Handsontable を使用するにあたり、その橋渡しのため少量の JavaScript コードは書くことになりますが、その JavaScript コードが上図のようにグローバルスコープに影響してしまう・漏れ出てしまうのはやむを得ないのでしょうか。

## JavaScript モジュールで解決

幸いなことに近代の クライアント Web アプリケーション開発・ブラウザ上の JavaScript 開発においては、**JavaScript モジュール**という仕組みが使えます (詳細は下記リンク先を参照)。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules

"JavaScript モジュール" の仕組みで JavaScript ファイル (.js) を扱うと、JavaScript ファイルを "モジュール" という単位で取得することになります。そして "モジュール" として取り込んだ JavaScript ファイル内で宣言・実装されている関数や変数は、**グローバルスコープに現れません**。モジュールとして読み込んだ側のみが、モジュール内の関数や変数を参照できます。また、そのようにしてモジュールとして読み込まれた JavaScript ファイル内で宣言・実装されている関数や変数は、既定ではモジュール外からは参照できません。**`export`** というキーワードで「このモジュールの外部に公開するよ」と指定された関数や変数のみが、モジュールを読み込んだ側に公開されます。

そして Blazor の JavaScript 相互運用機能もまた、JavaScript ファイルをモジュールとして読み込むことができます。

ということで、JavaScript モジュールの仕組みを使うように、前回の投稿記事からさらに、プログラムを変更してみます。

まず、`scripts.js` を読み込んでいる `index.html` 中の `<script>` タグですが、JavaScript ファイルはこのように読み込むのではなくて、Blazor (C#) 側から明示的に JavaScript モジュールとして読み込むようになるため、この `index.html` 中の `<script>` タグは (下記)、

```html:wwwroot/index.html
  ...
  <script src="libs/scripts/scripts.js"></script>  <!-- 👈これを... -->
</body>
</html>
```

削除します (下記)。

```html:wwwroot/index.html
  ...
  <!-- 削除! -->
</body>
</html>
```

次いで、`scripts.js` ですが、これを JavaScript モジュールとして読み込んだ際、このままではこの JavaScript ファイル中で実装されているいずれの関数・変数も、モジュール読み込みする側 (今回は Bloazr 側の C# コード) に公開されません。`createGrid` 関数は Blazor 側から呼び出したいので (下記)、

```js:wwwroot/libs/sctipts/scripts.js
function createGrid(element) {
  ...
```

`export` キーワードを `createGrid` 関数に追加して、この JavaScript モジュールの外部に公開します (下記)。

```js:wwwroot/libs/sctipts/scripts.js
// 👇 "export" を追加した!
export function createGrid(element) {
  ...
```

次は Blazor 側です。Blazor 側では、読み込んだ JavaScript モジュールを、`IJSObjectReference` インターフェース型で受け取ります。JavaScript オブジェクトへの参照を受け取るのと同じなんですね。ということで、読み込んだ JavaScript モジュールへの参照を保持する `IJSObjectReference` 型のフィールド変数を、Razor コンポーネントのコードブロックに追加します。

```csharp:Pages/Index.razor
...
@code {
    ...
    private IJSObjectReference? _jsModule; // これを追加
    ...
```

さていよいよ、`scripts.js` を JavaScript モジュールとして Blazor 側から読み込む処理です。やり方は、Blazor の JavaScript 相互運用機能を使って、"import" という機能を呼び出します。引数には、モジュールとして読み込む対象の JavaScript ファイルへの URL パスです。そしてその戻り値は `IJSObjectReference` インターフェース型とし、先に追加したフィールド変数にその戻り値を格納します。これが読み込んだ JavaScript モジュールへの参照となります (下記)。

```csharp:Pages/Index.razor
...
@code {
  ...
  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    ...
    // 👇 "import" ".js ファイルへの URLパス" を指定して JavaScript モジュールを読み込む
    _jsModule = await jsRuntime.InvokeAsync<IJSObjectReference>(
                                          "import", "./libs/scripts/scripts.js");
    ...
```

なお一点注意として、読み込む対象の JavaScript ファイルの "URL パス" というのは、Web ブラウザ上で実行されているときに、そのブラウザから見える・HTTP GET 要求で取得できる、そのような URL パスを指定する必要があります。

そして最後に、`IJSRuntime` サービスを介してグローバルスコープに露出していた `createGrid` JavaScript 関数を呼び出していた箇所を (下記) ...

```csharp:Pages/Index.razor
...
@code {
  ...
  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    ...
    _jsModule = await jsRuntime.InvokeAsync<...>("import", "./libs/scripts/scripts.js");

    // IJSRuntime を使ってグローバルスコープにある createGrid 関数を呼び出し
    //                 👇
    _grid1 = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid", _div1Ref);
    _grid2 = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid", _div2Ref);
    ...
```

"import" で読み込んだ JavaScript モジュールへの参照を使って、モジュール内で公開 (`export`) されている `createGrid` JavaScript 関数を呼び出すようにします。

```csharp:Pages/Index.razor
...
@code {
  ...
  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    ...
    _jsModule = await jsRuntime.InvokeAsync<...>("import", "./libs/scripts/scripts.js");

    // 上の行で読み込んだ JavaScript モジュールへの参照を使って、
    // JavaScrip モジュール内で公開(export)されている createGrid 関数を呼び出し
    //                 👇
    _grid1 = await _jsModule.InvokeAsync<IJSObjectReference>("createGrid", _div1Ref);
    _grid2 = await _jsModule.InvokeAsync<IJSObjectReference>("createGrid", _div2Ref);
    ...
```

仕上げに、読み込んだ JavaScript モジュールへの参照は、不要になったら廃棄しておきましょう。

```csharp:Pages/Index.razor
...
@code {
  ...
  public async ValueTask DisposeAsync()
  {
    if (_jsModule != null) {
      try { await _jsModule.DisposeAsync(); } 
      catch (JSDisconnectedException) { }
    }
    ...
  }
}
```

これで完成です!

実際に実行してみると、もちろん Handsontable グリッドはちゃんと機能しつつ、この変更前にはグローバルスコープに漏れ出ていた `createGrid` JavaScript 関数が、ここまでの変更によってグローバルスコープから見えなくなりました! (下図)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/e96eea04-0458-b431-4095-7b82bccff0bc.png)

## まとめ

Blazor の JavaScript 相互運用からでも利用できる **JavaScript モジュール**の仕組みを使うことで、グローバルスコープへの露出をさらになくすことができました。**モジュールとして読み込まれる JavaScript ファイル内の変数や関数は、グローバルスコープに出現しない**ためです。

Blazor 側では JavaScript ファイルをモジュールとして読み込むには **`"import", "<読み込むJavaScript ファイルへの URL パス>"`** を実行します。すると、戻り値に、読み込んだ JavaScript モジュールへの参照が `IJSObjectReference` インターフェース型で返ってきます。`IJSObjectReference` インターフェースを介して、読み込んだ JavaScript モジュール内で **`export`** されている関数を実行することができます。

以上のとおり JavaScript ファイルの (モジュールとしての) 読み込みは、Blazor 側の C# コード上で実装するので、フォールバックページである **`index.html` 中に `<script>` タグを追記する必要もなくなりました**。

今回の例程度ですとそこまでありがたみが湧かないと思いますが、以上のようにモジュール方式で実装してあると、JavaScript を同梱した再利用可能なコンポーネントを NuGet パッケージ化して共有・配付する場合に、いちいち「index.html に、かくかくしかじかの script タグを追記してください」とお願いする必要がなくなります。加えて、その NuGet パッケージを利用する側の開発者が、`<script>` タグの追記を忘れたり、.js ファイルへのパスに誤りがあると動かない、という**トラブルからも解放**されます。

ということで、Blazor における JavaScript 相互運用の利用をしっかり完成度を上げる際には、JavaScript モジュール方式で読み込み利用するのがよいでしょう。

_Learn, Practice, Share!_