ASP.NET Core上でSPAを動かしたい

フロントエンドをAngular等のSPAフレームワーク、バックエンドをASP.NET Coreという環境を作るためのTipsです。

実はこれから紹介することをしなくてもはじめからがっちゃんこされた状態のものが配布されています。
が、中をよく見てもイマイチよくわからなかったので自分でがっちゃんこさせてみました。

# とりあえず先にそれくれよ
https://blogs.msdn.microsoft.com/webdev/2017/02/14/building-single-page-applications-on-asp-net-core-with-javascriptservices/

# やりたいこと

- サーバーはASP.NET Coreで使っているKestrelを使いたい
- `@angular/cli`を使ってビルドされたものをフロントとして使いたい
- AngularのRouting機能を使ってページ遷移したい

# 環境

- dotnet cli 1.1.0
- `@angular/cli` 1.0.0

# ASP.NET Coreプロジェクトの作成

Visual Studioから簡単に作れるのですが、ここではコマンドで作ってみます。

```bash
mkdir server
cd server
dotnet new webapi
dotnet restore
```

## ライブラリ追加

SPAを動かすためにいくつか必要なライブラリを追加します。

```server.csproj.diff
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
+    <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="1.1.0" />
+    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
  </ItemGroup>

</Project>
```

csprojを編集したあと再び`dotnet restore`コマンドを打ちます。

## Startup.csの編集

```Startup.cs.diff
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
     loggerFactory.AddConsole(Configuration.GetSection("Logging"));
     loggerFactory.AddDebug();

+    app.UseStaticFiles();

-    app.UseMvc();
+    app.UseMvc(routes =>
+    {
+        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
+        routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
+    });
}
```

[以前の自分の記事「Slackで特定のキーワードに反応して自動的にReactionをつけるBotを作った」](http://qiita.com/kuluna/items/bda141eafc968d4810f7#startupcs)でも書きましたが、app.UseXXXには__呼び出す順番があります。__ひとまずは上の順番の通りにしてください。

ポイントはUseMvcの中でやっていることです。ここでルーティングを設定しています。

まず、`MapRoute`ではASP.NET Core MVCのControllerにアクセスするためのルーティング設定です。URLでルートパスを指定されたときは`Home`と名のついたControllerの`Index`メソッドを実行するという設定になっています。なので、この後にHomeControllerを作成します。
次に`MapSpaFallbackRoute`でもHomeControllerのIndexを指定しています。両方同じことをやっているように見えますが、実はこれがSPAで重要な設定になります。

### MapSpaFallbackRouteの役割

まずAngular側でもルーティングの仕組みがあります。URLも`/page1`のように書き換わります。しかし実際はindex.htmlの中でjavascriptをうまく使いあたかもそのURLに遷移しているかのように見せているだけです。(だからSPAなんですけどね)
では一方でそのURLにそのままアクセスしてみるとどうでしょうか。Angularはindex.htmlしかありません。なのに`/page1`とかにアクセスされてもpage1/index.htmlはありません。なのでこの場合404を返してしまいます。
それを防ぐのが`MapSpaFallbackRoute`の役割です。これは存在しないパスを指定されたときに、URLは変えずにとりあえず`Home`Controllerの`Index`メソッドを実行するという設定です。

つまり`HomeController`の`Index`メソッドにAngularのindex.htmlを返すようにすれば全てのルーティングが上手くいくという設計になっているのです👏

## HomeController.csの追加
既に`Controllers`フォルダがあると思いますが、そのフォルダに新しく`HomeController.cs`を追加します。

```HomeController.cs
using Microsoft.AspNetCore.Mvc;

namespace server.Controllers
{
    /// <summary>
    /// Default MVC Controller
    /// </summary>
    public class HomeController : Controller
    {
        /// <summary>
        /// Open the wwwroot/index.html
        /// </summary>
        public IActionResult Index() => File("/index.html", "text/html");
    }
}
```
namespaceは自分の環境に合わせてください。
ここでやっていることは単純に`wwwroot/index.html`をレスポンスとして返す処理をしています。あとはwwwrootフォルダにAngularを置けば良いということですね。もしwwwrootフォルダがなければcsprojのあるフォルダから新しくwwwrootという名前でフォルダを作っておきましょう。

これでASP.NET Core側は完了です。

# Angularプロジェクトの作成

次にAngular側を作っていきましょう。作業フォルダはどこでもOKです。

```bash
ng new --routing client
cd client
ng generator component page1
```

`client`の部分がアプリ名になるので好きなものに変更します。
ルーティング用に新たにpage1というコンポーネントを作成しています。

## app-routing.module.tsの編集

--routing オプションをつけているとappフォルダにこのファイルが作成されます。ここにpage1へのルーティングを追加しましょう。

```app-routing.module.ts.diff
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

+ import { Page1Component } from './page1/page1.component';

const routes: Routes = [
  {
+    path: 'page1',
+    component: Page1Component
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```

これで終わりです。

## Publish

```bash
ng build --prod
```

で`dist`フォルダの中に作ったフロント一式が入ってますので、これをASP.NET Coreの`wwwroot`フォルダにコピーします。

# 実行

ASP.NET Coreのフォルダにcdして

`dotnet run`

でサーバーが起動します。`http://localhost:5000/`でブラウザにアクセスするとAngularのページが表示され、`http://localhost:5000/page1`にアクセスしても404にならずページが表示されていると思います。
また、`http://localhost:5000/api/values`にアクセスするとJSONがとれますが、これはASP.NET Coreの`ValuesController`がリクエストを返しています。もちろんAngularからAPIにアクセスすることもできます。