2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BlazorでTypeScriptやnpmを自然な使い勝手でVisual Studioと統合して使いたい

Last updated at Posted at 2025-12-14

導入

BlazorでTypeScriptを使いたい、よくある希望かと存じます。
かと言ってここでNodeJSをただただ導入しようとするとビルドの構成だとか、Blazor特有の事情(wwwrootPages/xxx.razor.jsの2か所にJavaScriptを配置できる)を考慮する必要があるなど以外と面倒です。

今回はこれをなるべくVisual Studioと統合した形で自然に管理する方法ができたので皆様にご紹介いたします。

何をやるのか?

  • Microsoft.TypeScript.MSBuildでTypeScriptをコンパイル
  • Microsoft.TypeScript.MSBuildtsconfig.jsonを使ってwwwroot上のTSとコンポーネントに紐づくxxx.razor.ts間で自動補間を使えるようにする
  • esbuildを使ってnpmパッケージをwwwroot上に展開し、wwwroot上のTSやxxx.razor.tsファイルから参照できるようにする(追加でMSBuildから呼び出せるようにする)

事前準備

  • nodenpmのインストール

やり方

1. TypeScript コンパイラを追加してTSファイルからBlazorで使えるJSファイルへコンパイルする仕組みを用意する

自身のTSを使いたいBlazorプロジェクト(以下プロジェクトと呼称)にMicrosoft.TypeScript.MSBuildを追加します。

dotnet add package Microsoft.TypeScript.MSBuild

次にtsconfig.jsonファイルを、プロジェクトのルートフォルダーに作成します。

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "removeComments": false,
    "moduleResolution": "Bundler",
    "skipLibCheck": true,
    "noImplicitAny": true,
    "strict": true,
    "lib": [
      "DOM",
      "ESNext"
    ],
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "/js/*": [
        "./wwwroot/js/*"
      ],
      "/*": [
        "./*"
      ]
    }
  },
  "exclude": [
    "node_modules",
    "bin",
    "obj"
  ]
}

こちらを使うで
アプリをビルドするたびに、
プロジェクトに配置したTypeScriptファイルの隣に
JSファイルが生成されるようになります。

ComponentとまとめるタイプのJSの場合

image-3.png

image-4.png

wwwrootに配置するタイプのJSの場合

image-9.png

image.png

※今回は、wwwroot上のTSファイルは、wwroot/jsフォルダーへ配置する形で作ります。
PJのフォルダー構成が異なる場合は適宜読み替えてください。

image-12.png

重要なポイントを紹介します。

tsconfig.jsonの一部抜粋
    "paths": {
      "/js/*": [
        "./wwwroot/js/*"
      ],
      "/*": [
        "./*"
      ]
    }
  },
  "exclude": [
    "node_modules",
    "bin",
    "obj"
  ]

Blazorプロジェクトは通常のJSのプロジェクトと違い
Blazorコンポーネントとまとめて配置するタイプのJSとwwwroot上に配置するJSの二つがあります。

そしてこれらはパスの解決方法も微妙に異なります。

フォルダー階層 サーバー上のパス
wwwroot/js/util.js js/util.js
Pages/Home.razor.js Pages/Home.razor.js

そこで今回は、パス解決ルールを定義し、import文を書く際のパスを実際のサーバー上に配置されたときのパスと一致するように設定しました。

tsconfig.jsonの一部抜粋
      "/js/*": [
        "./wwwroot/js/*"
      ],
      "/*": [
        "./*"
      ]

これでこんな感じに書いたwwwroot/js上のtsファイルが

wwwroot/js/util.ts
/**
 * 値を===で囲んだ文字列に変換する
 * @param value 値
 * @returns === ${値} ===
 */
export const cover = (value: string) => ` === ${value} ===`;

こんな感じに補間が効いた状態かつビルド後のパスで呼び出せるようになります。

Pages/Home.razor.ts
import { cover } from '/js/util.js' // ← js拡張子必須

export const hello = (name: string) => {
    console.log(cover(`${name} こんにちは`));
}

ちゃんとドキュメントコメントもついてます。

image-10.png

2. esbuildを使ってnpmライブラリーを参照できるようにする

今回の登場人物はこちらです。
image-11.png

赤がこれから編集するファイル、
青がそれによって自動生成されるファイルです。

まずはpackage.jsonを用意します。
esbuildが使いたいだけなので最小です。

package.jsonでesbuildを用意する

package.json
{
  "scripts": {
    "build:libs": "esbuild JsLibraries/libs.ts --bundle --format=esm --outfile=wwwroot/js/vendor/libs.js"
  },
  "dependencies": {
    "esbuild": "^0.27.1"
  }
}

コマンドを実行してesbuildをインストールします。

npm install

JsLibraries/libs.tsに記載した内容をesm形式で wwwroot/js/vendor/libs.jsに書きだすスクリプトを記載しています。

npmでライブラリーを追加する

次にライブラリーをインポートします。
なんでも良いので、今回はnanoidというユニークなIDを作れるライブラリーを追加してみます。

npm install nanoid

次にこのnanoidをBlazorで読み込むためフォルダーを用意しましょう。

プロジェクトのルートフォルダーにJsLibrariesフォルダーを作成、その中にlibs.tsというファイルを作成します。

こんな形で追加したライブラリーを再exportします。

JsLibraries/libs.ts
export { nanoid } from 'nanoid';

tsconfig.jsonに設定を追加します。

tsconfig
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "removeComments": false,
    "moduleResolution": "Bundler",
    "skipLibCheck": true,
    "noImplicitAny": true,
    "strict": true,
    "lib": [
      "DOM",
      "ESNext"
    ],
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
+     "/js/vendor/libs.js": [
+       "./JsLibraries/libs.ts"
+     ],
      "/js/*": [
        "./wwwroot/js/*"
      ],
      "/*": [
        "./*"
      ]
    }
  },
  "exclude": [
    "node_modules",
+   "JsLibraries/libs.ts",
    "bin",
    "obj"
  ]
}

最後にプロジェクトの設定ファイルに以下の設定を追加し、Microsoft.TypeScript.MSBuildのビルドシステムが動く前にesbuildが先ほどのlibs.tsをビルドするように設定します。

プロジェクトのcsprojの一部抜粋
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
	<PropertyGroup>
		<TargetFramework>net10.0</TargetFramework>
		<Nullable>enable</Nullable>
		<ImplicitUsings>enable</ImplicitUsings>
		<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
		<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all" />
		<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.9.3">
			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
			<PrivateAssets>all</PrivateAssets>
		</PackageReference>
	</ItemGroup>
    
+	<Target Name="BuildJsLibraries" BeforeTargets="CompileTypeScript">
+		<Exec Command="npm run build:libs" />
+	</Target>

    

この状態でビルドするとファイルが生成されます。

あとはこのファイルをwwwroot上のTSファイルなんかで読み込めば完了です。

image-14.png

ちゃんとコメントも見られます。

image-18.png

先ほどのutil.tsで読み込んでみましょう。
loggerというメソッドを作成しました。

wwwroot/js/util.ts
import { nanoid } from '/js/vendor/libs.js'

/**
 * 値を===で囲んだ文字列に変換する
 * @param value 値
 * @returns === ${値} ===
 */
export const cover = (value: string) => ` === ${value} ===`;

+ /**
+  * ログ形式の文字列を作成する
+  * @param category カテゴリー
+  * @param value 出力
+  * @returns 成形されたログ文字列
+  */
+ export const logger = (category: string, value: string) => `${nanoid} [${category}] ${value}`;

このファイルをいくつかの方法で読み込んでみます。

wwwroot/LessenCode2.lib.module.ts(ブラウザー上でBlazor読み込み時に自動で読み込まれるJSファイル)
import { logger } from './js/util.js' // ← js拡張子必須

console.log(logger("lib.module.ts", "初期化されました"))
Pages/Home.razor.ts
import { logger } from '/js/util.js'

export const hello = (name: string) => {
    console.log(logger("Hello", `${name} こんにちは`));
}
Pages/Home.razor
@page "/"
@inject IJSRuntime JS
<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.


@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {

        if (!firstRender) { return; }

        // Home.razor.ts
        await using var helloModule = await JS.InvokeAsync<IJSObjectReference>("import", "/Pages/Hello.razor.js");
        await helloModule.InvokeVoidAsync("hello", "世界");

        // util.ts
        await using var utilModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/util.js");
        var loggerString = await utilModule.InvokeAsync<string>("logger", "Hello.razor", "util直接呼出し");
        Console.WriteLine(loggerString);

        // libs.ts
        await using var libModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/vendor/libs.js");
        var nanoid = await libModule.InvokeAsync<string>("nanoid");
        Console.WriteLine($"nanoid = {nanoid}");

    }
}

image.png

上から順に
LessenCode2.lib.module.ts

Home.razor.ts(Home.razorファイルから呼び出し)

util.ts(Home.razorファイルから呼び出し)

libs.ts(Home.razorファイルから呼び出し)
の3パターンでnanoidが読み込まれている事を確認できました。

解決できていない点

libs.tsが、esbuildMicrosoft.TypeScript.MSBuild両方からビルドされてしまってます。

image-17.png

Microsoft.TypeScript.MSBuildからは、こんな形でビルドされます。

export { nanoid } from 'nanoid';
//# sourceMappingURL=libs.js.map

こちらに関して、wwwroot上の生成物はesbuildによって生成されたwwwroot/js/vendor/libs.jsであり、
Microsoft.TypeScript.MSBuild製の生成物はwwwroot上には残らず、ビルド成果物に公開されないため無視してます。

まとめ

今回はBlazorでTypeScriptを使う際にパスを通しつつ、npmまで使えるようにできました。
個人的に補完ができる自然な使い勝手で統合ができたところはうまくできたと自負しております。
一方でesbuildMicrosoft.TypeScript.MSBuild両方からビルドされてしまったのは何とかしたかったなと思うところです。

今回の記事は下記の参考文献がきっかけで生まれました。
先駆者様、そしてこのアドベントカレンダーを主催していただきましたjsakamoto氏、そしてこの記事を読んで下さった皆様に感謝を表し終わりたいと思います。

ありがとうございました。

参考文献

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?