BlazorでTypeScriptを利用する

この記事は [C# Advent Calendar 2020](https://qiita.com/advent-calendar/2020/csharplang) 22日目の記事です。   
ここで紹介したサンプルプロジェクトは、GitHubで公開しています。
[Sumples/TypeScriptTest at master · Atria64/Samples](https://github.com/Atria64/Samples/tree/master/TypeScriptTest)

# はじめに
今回はC#でのSPA開発が可能となる画期的なフレームワーク「Blazor」を話題に記事を投稿しようと思います。
Blazorが何かよくわからない方は、以下の記事を参照すると良いかもしれません。

 - [いいからBlazorをさわるんだ - Qiita](https://qiita.com/proprogrammer0/items/d61587b95ea987318a35)
 - [Blazor をお勧めできる人は誰か? - Qiita](https://qiita.com/jsakamoto/items/52c5ef52fc5ccebaae1f)

また、Blazorを利用すると以下のようなものが作れます。

 - [Atria64/CUIPortfolio: CUI風Portfolio](https://github.com/Atria64/CUIPortfolio)
    - Blazorで作成したCUI風のサイトです
 - [Atria64/RandomSushi](https://atria64.github.io/RandomSushi/)
    - 某スシローのメニューからランダムで選定する~~クソ~~アプリです

しかしここで断っておくと、初学者にとってこの記事の内容は少し難しいかもしれません。(難しく書いてすみません)
雰囲気だけでも楽しんでいただけたらなと思います。

# BlazorでTypeScriptを導入する上で注意しなければならない事
はじめに注意しておかなければならないことは、**C#とTypeScript間に型情報などは渡せない**ということです。
**「じゃあ意味ねぇじゃねぇか」**という人もいるかもしれませんが、少なからずメリットもあります。
### Blazor/TypeScriptが提供するもの
Blazor/TypeScriptの構成では残念ながらC#/TypeScript間で型情報を渡すことができません。
しかし、**TypeScript自身の型システム**や**コンパイル時のチェックの恩恵を受けることができます。**
また、JavaScript恐怖症の方やTypeScriptを愛する人にとって、BlazorでTypeScriptを利用することができるというのは朗報であるはずです。

# 環境
 - Visual Studio 2019
 - ASP.NET Core 3.1.9 
 - Microsoft.AspNetCore.Components.WebAssembly.* 3.2.1
 - Microsoft.TypeScript.MSBuild 4.2.1

# 導入方法
### 今回目指す動作
今回はTypeScriptを用いて、以下2つの動作をさせるのを目的とします。

 - ボタンを押したときに、テキストエリアに書かれている文字列をalertする。(返り値なしの関数呼び出し)
 - ボタンを押したときに、テキストエリアに書かれている文字列を加工してボタン名を変更する。(返り値ありの関数呼び出し)

ここまで簡単な操作であればTypeScriptを使うまでもありませんが、サンプルということでご容赦ください。

## プロジェクトの作成
`新しいプロジェクトの作成→Blazorプロジェクト→Blazor WEBAssembly App` の順にプロジェクトを作成します。
今回ここで作成するプロジェクト名は `TypeScriptTest` にします。
詳しくは割愛しますので、他のサイトなどを御覧ください。

## テスト用ページの作成
TypeScriptを呼び出すためのテストページを作成します。
`Pages`から`Index.razor`ファイルを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/52db25cd-7aea-8e1c-c446-062590c767a1.png)

今回は2つのボタンと一つのテキストエリアを必要とするため、以下のようにrazorファイルを書き換えます。

```html:index.razor
@page "/"

<h1>TypeScriptTest</h1>

<input @bind="text" @bind:event="oninput" />
<button @onclick="TsAlertClick">TsAlert</button>
<button @onclick="TsEditTextClick">@TsEditText</button>

@code{
    string text;
    string TsEditText = "TsEditText";

    private void TsAlertClick()
    {
        
    }

    private void TsEditTextClick()
    {
        TsEditText = "hoge";
    }
}
```
起動すると以下のような画面が表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/1cca2e78-245a-df56-bfb6-11638fb621b1.png)

## TypeScriptファイルの作成
新しい項目の追加(Ctrl+Shift+A)から`wwwroot`直下にTypeScriptファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/f40b3161-8129-612b-4038-94ff91f4424a.png)
今回は`TypeScript.ts`とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/df0946fa-54b4-8c8a-80f0-955e287c022a.png)
TypeSctipr.tsの中身は以下のようにします。

```ts:TypeScript.ts
namespace Test {
    class Functions {
        //与えられた文字列をalertで表示します
        public showAlert(message: string){
            alert(message);
        }
        //与えられた文字列を編集して返します。
        public editText(message: string): string {
            return message+" : edited";
        }
    }
    export function Load(): void {
        window['Functions'] = new Functions();
    }
}

Test.Load();
```

## ビルドの構成
TypeScriptファイルが完成したので、ビルドの構成を行います。
まずはじめに`Microsoft.TypeScript.MSBuild`を追加します。

### Microsoft.TypeScript.MSBuildの追加
NuGetパッケージマネージャを起動して、検索欄に`Microsoft.TypeScript.MSBuild`と入力します。
そして執筆時点での最新バージョンである`Microsoft.TypeScript.MSBuild4.1.2`をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/412a7806-8d3e-0b89-672d-e1cd6a465be7.png)
インストール完了後、`.csproj`ファイルを編集します。

### .csprojファイルの編集
.csprojファイルに、TypeScriptファイルをコンパイルするような命令を記述します。

```xml:TypeScriptTest.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
    <RazorLangVersion>3.0</RazorLangVersion>
    <TypeScriptToolsVersion>4.0</TypeScriptToolsVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.1" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.1" PrivateAssets="all" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.1" PrivateAssets="all" />
    <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.1.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
  </ItemGroup>

  <ItemGroup>
    <TypeScriptCompile Include="wwwroot\TypeScript.ts" />
  </ItemGroup>

</Project>

```

`<PropertyGroup></PropertyGroup>`には`<TypeScriptToolsVersion>4.0</TypeScriptToolsVersion>`を追加してあげることで、プロジェクトのコンパイル時に使用するTypeScriptのバージョンをMSBuildに通知させます。
これを記述しなかった場合、MSBuildはシステムにインストールされている最新バージョンを利用します。

また、`<ItemGroup></ItemGroup>`に`<TypeScriptCompile Include="wwwroot/TypeScript.ts" />`を追加することで、MSBuildにどのファイルをビルドするかを通知させます。

ここで一度ビルドを走らせてみましょう。
うまくビルドが走った場合、`TypeScript.map`と`TypeScript.js`が新しく`wwwroot`直下に生成されているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/6a2dd1c6-914c-70cb-1e90-e2e5d5ccb939.png)


## TypeScriptでコンパイルしたJavaScriptを呼び出す
あとは従来のJavaScriptをInvokeする手順とほとんど一緒です。

### index.htmlへのタグ追加
`wwwroot`直下の`index.html`に`<script src="TypeScript.js"></script>`の一行を追加します。

```html:index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>TypeScriptTest</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
</head>

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="TypeScript.js"></script>
</body>

</html>

```

### IJSRuntimeを用いた呼び出し
IJSRuntimeをinjectした後、`InvokeVoidAsync`や`InvokeAsync<T>`を利用してJavaScriptメソッドを呼び出します。
コードは以下のようになります。

```html:index.razor
@inject IJSRuntime JSRuntime
@page "/"

<h1>TypeScriptTest</h1>

<input @bind="text" @bind:event="oninput" />
<button @onclick="TsAlertClick">TsAlert</button>
<button @onclick="TsEditTextClick">@TsEditText</button>

@code{
    string text;
    string TsEditText = "TsEditText";

    private void TsAlertClick()
    {
        JSRuntime.InvokeVoidAsync("Functions.showAlert", text);
    }

    private async Task TsEditTextClick()
    {
        TsEditText = await JSRuntime.InvokeAsync<string>("Functions.editText", text);
    }
}
```
### JavaScriptの呼び出しがイマイチわからない場合
僕の説明がわかりにくかった場合は以下の記事などを参考にすると良さそうです。

 - [ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す | Microsoft Docs](https://docs.microsoft.com/ja-jp/aspnet/core/blazor/call-javascript-from-dotnet?view=aspnetcore-5.0)
 - [BlazorアプリケーションでC#のコードからJavaScriptを呼び出す](https://www.ipentec.com/document/csharp-blazor-application-call-javascript-from-csharp-code)


# 動作確認
実際にビルドし実行するとTypeScriptのコンパイラが走りJavaScriptが生成され、うまく呼び出すことができるようになっています。
## テキストエリアに書かれている文字列のalert
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/10203a19-847f-5e4f-0102-40a36c1d5e83.png)
## テキストエリアに書かれている文字列を加工したボタン名の変更
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86472/00d50363-ab5a-b517-4db8-1f37aa802499.png)

# おわりに
今回はBlazorにTypeScriptを導入する試みを行ってみました。
この知見が誰かの役に立つことがあれば幸いです。

また、今回作成したプロジェクトはGitHubでも公開しておりますので、ぜひお役立てください。
[Sumples/TypeScriptTest at master · Atria64/Samples](https://github.com/Atria64/Samples/tree/master/TypeScriptTest)

次回の [C# Advent Calendar 2020](https://qiita.com/advent-calendar/2020/csharplang) 23日目は @nqdior さんです！
 
# 参考文献
- [Getting Started with TypeScript for JSInterop in Blazor](https://chrissainty.com/getting-started-with-typescript-for-jsinterop-in-blazor/)
- [Getting Started with TypeScript for JSInterop in Blazor - Reddit](https://www.reddit.com/r/Blazor/comments/bb5ma6/getting_started_with_typescript_for_jsinterop_in/)