3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

BlazorAdvent Calendar 2022

Day 23

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

Posted at

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

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

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

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

以上のように変更をしてきたのですが、しかし、上記 2 記事を経てもなお、まだ 1 つだけ、JavaScript のグローバルスコープに露出があります。scripts.js 内で定義した createGrid 関数です (下図参照)。
image.png

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

JavaScript モジュールで解決

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

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

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

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

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

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

削除します (下記)。

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

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

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

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

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

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

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

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

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 関数を呼び出していた箇所を (下記) ...

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 関数を呼び出すようにします。

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 モジュールへの参照は、不要になったら廃棄しておきましょう。

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

これで完成です!

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

image.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!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?