ASP.NET Core で AngularJS + TypeScript する


ASP.NET Core で AngularJS + TypeScript してみる。

2016/07/05 追記
ASP.NET Core 1.0 が RTM になったので内容を更新しました。

# 開発環境
Windows 10 Pro
Visual Studio 2015 Community Update 3
.NETCoreApp,Version=v1.0

# 準備
今回は TypeScript の型定義ファイルの管理に [typings](https://www.npmjs.com/package/typings) を使いますので未インストールの場合は入れておいてください。

```bash
npm install -g typings
```

# プロジェクトの作成
ASP.NET Core Web API プロジェクトを作成します。

[新しいプロジェクト] - [Web] - [APS.NET Core Web Application(.NET Core)] を選択して、テンプレートは [Web API] を選択します。

プロジェクトを作成したら、まずは Server-side 側から実装しています。

# Server-side

## パケージの入手
今回は Razor を使わないで実行時に直接 html ファイルを表示します。

ASP.NET Core アプリケーションで静的ファイルを扱うために Microsoft.AspNetCore.StaticFiles をプロジェクトにインポートします。 project.json の 'dependencies' に直接追加するか、パッケージマネージャーコンソールに以下のコマンドを入力してインポートしてください。

Install-Package Microsoft.AspNetCore.StaticFiles

```json:project.jsonの一部
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0"
  },
```

project.json の `dependencies` が上のようになっていればOKです。
インポートしたら次は静的ファイルを扱うための処理を追加します。

## 静的ファイルを扱う
Client-side のフォルダ構成を以下のようにしたいと思います。

```
wwwroot
├─app
│  ├─scripts            <-- ts/js ファイル
│  │  └─controllers
│  │        main.ts
│  │        about.ts
|  |
│  │    app.ts
|  |
│  ├─styles            <-- less/css ファイル
│  │    _common.less
│  │    about.less
│  │    app.less
│  │    main.less
|  |
│  └─views            <-- テンプレート html 
│       main.html
│       about.html
|
│    index.html
|
└─bower_components    <-- bower で管理しているパッケージ
    ├─angular
    ~
```

Microsoft.AspNetCore.StaticFiles に定義されている拡張メソッド `UseDefaultFiles()` と `UseStaticFiles()` を使って  Client-side で作成する静的ファイルを扱えるよう Startup.cs に処理を追加します。上記メソッドで扱えるようになる静的ファイルはデフォルトだと wwwroot フォルダから見たパスを指定して読込みをおこなうことができます。例えば、 js ファイルを html の script 要素で読込む時は `src="app/scripts/hoge.js"` と指定します。

今回は index.html が wwwroot/app フォルダにあるので静的ファイルを読込むときに、この html ファイルのあるディレクトリからの相対パスで指定できるようにします。

追加する処理は以下となります。

1. 実行時に表示する html の指定  
今回は wwwroot/app に index.html を作成します。デフォルトだと実行時に wwwroot 直下の index.html を探しに行って404 されてしまうので、 wwwroot/app にある index.html を表示するようにします。
2. JS ファイルを呼び出すときのパスの指定  
js ファイルを html の script 要素で読込むときに `src="scripts/hoge.js"` と指定して扱えるようにします。
3. css ファイルを呼び出すときのパスの指定  
css ファイルを html の link 要素で  `href="styles/fuga.css"` と指定して扱えるようにします。
4. html テンプレートを呼び出すときのパスの指定  
AngularJS の ngRoute を使ってルーティングする際に、指定するテンプレートのパスを view/piyo.html と指定して扱えるようにします。

以下コードです。

```csharp:Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using System.IO;

namespace AspAngularApp
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc();

            // プロジェクトで扱う各種静的ファイルの場所を指定する

            // js/css ファイルの読込み先
            var location = "wwwroot/app";

            // 実行時に表示する html ファイルを指定する
            var options = new DefaultFilesOptions();
            options.DefaultFileNames.Clear();
            options.DefaultFileNames.Add("app/index.html");
            app.UseDefaultFiles(options);

            app.UseStaticFiles();

            // javascript
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), $"{location}/scripts")),
                RequestPath = new PathString("/scripts"),
            });

            // css
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), $"{location}/styles")),
                RequestPath = new PathString("/styles")
            });

            // html テンプレート
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), $"{location}/views")),
                RequestPath = new PathString("/views")
            });
        }
    }
}
```

ts/css ファイルや pug (jade) ファイルなどコンパイルしたファイルを .tmp フォルダに出力する場合や、js/css ファイル を minify して dist フォルダに出力したりする場合は `location` に設定されているパスを変更すれば Client-side の処理を変更しなくても切替えができます。

## プロジェクトのプロパティをいじる
起動時のURLがデフォルトだと api/values となっていますのでこれを変更します。

ソリューションエクスプローラでプロジェクトのフォルダーを右クリックして表示されるコンテキストメニューからプロパティを選択して表示されたタブから[デバッグ]を選択して[起動URL] のテキストボックスを空にして保存します。

以上でServer-side の実装は終了です。Web API についてはプログラム作成時にデフォルトで追加されている ValuesController.cs をそのまま使います。

続いて Client-side の実装に入っていきます。

# Client-side

## 構成ファイルの準備
以下のファイルを作成します。
1. npm 構成ファイル
2. Gulp 構成ファイル
3. Bower 構成ファイル

### npm 構成ファイル (package.json) の作成
今回 npm でインストールするパッケージは以下となります。

|  パッケージ名       | 機能                                                                         |
| :------------------ | :--------------------------------------------------- |
| gulp                       | タスクランナー                                                       |
| gulp-less               | less をコンパイルするプラグイン                           |

プロジェクトのディレクトリに package.json を作成します。

```json:package.json
{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {},
  "dependencies": {
    "gulp": "^3.9.1",
    "gulp-less": "^3.1.0"
  }
}
```

### Gulp 構成ファイル (gulpfile.js) の作成
今回作成するタスクは以下となります。

| タスク名   | 機能                                                 |
| :---------- | :----------------------------------- |
| less           | less ファイルをコンパイルする。  |
| watch        | ファイルの変更を監視する。         |
| build          | ビルドする。                                 |

プロジェクトのディレクトリに gulpfile.js を作成します。

```javascript:gulpfile.js
var gulp = require('gulp');
var less = require('gulp-less');

// LESS をコンパイルする
gulp.task('less', () => {
    gulp.src(['./wwwroot/app/**/*.less', '!./wwwroot/app/**/_*.less'])
        .pipe(less())
        .pipe(gulp.dest('./wwwroot/app'));
});

// watch
gulp.task('watch', () => {
    gulp.watch('./wwwroot/app/**/*.less', () => {
        gulp.run('less');
    });
});

// ビルド
gulp.task('build', ['less']);
```

gulpfile.js を保存したら、タスクランナー エクスプローラを表示して、タスクの一覧から `build` タスクを右クリックして 表示される コンテキストメニューから [Bindings] -  [ビルド前] に設定するか、 `watch` タスクを [Bindings] - [プロジェクトを開く] に設定しておくとよいと思います。

### Bower 構成ファイル (bower.json) の作成
今回使用するパッケージは以下となります。

* bootstrap
* angular
* angular-route

プロジェクトのディレクトリに bower.json と .bowerrc を作成します。

```json:.bowerrc
{
  "directory": "wwwroot/bower_components"
}
```

```json:bower.json
{
  "name": "asp.net",
  "private": true,
  "dependencies": {
    "bootstrap": "^3.3.6",
    "angular": "^1.5.7",
    "angular-route": "^1.5.7"
  }
}
```

## 型定義ファイルの入手
今回使う定義ファイルは以下となります。
* angular
* angular-route
* jQuery

プロジェクトのディレクトリでコマンドプロンプトからコマンドを実行して必要な型定義ファイルを入手します。

```bash
typings init
typings install dt~jquery dt~angular dt~angular-route  --global --save
```

実行するとプロジェクトフォルダの下に typings フォルダが作成されてそこに型定義ファイルがインストールされます。

## Module の作成
app モジュールの作成と、画面遷移のルーティングをします。

wwwroot/app/scripts フォルダに app.ts を作成します。

```javascript:app.ts
///<reference path="../../../typings/index.d.ts" />

'use strict';

// モジュール
angular.module('app', ['ngRoute'])
    // ルート情報
    .config(($routeProvider: ng.route.IRouteProvider) => {
        $routeProvider
            .when('/', {
                templateUrl: 'views/main.html',
                controller: 'MainCtrl',
                controllerAs: 'main'
            })
            .when('/about', {
                templateUrl: 'views/about.html',
                controller: 'AboutCtrl',
                controllerAs: 'about'
            })
            .otherwise({
                redirectTo: '/'
            });
    });
```

## Controller の作成

wwwroot/app/scripts/controllers フォルダに main.ts と about.ts を作成します。

まず、main.ts を作成します。

```javascript:main.ts
///<reference path="../app.ts" />

'use strict';

namespace SampleAngularApp.Controllers {
    export class MainCtrl {

        /**
         * タイトル
         */
        title: string = 'main';

        /**
         * Getリクエストで受取る JSON オブジェクト
         */
        values: Array<string>

        /**
         * コンストラクタ
         * @param $http IHttpService
         */
        constructor(private $http: ng.IHttpService) {

            this.awake();
        }

        /**
         * 呼び出し時の処理
         */
        private awake() {

            // GET
            this.$http.get('api/values')
                .success((data: Array<string>) => this.values = data);
        }
    }
}

angular.module('app')
    .controller('MainCtrl', SampleAngularApp.Controllers.MainCtrl);
```

ViewModel に 画面に表示するタイトルと Server-side で定義されている Web API (api/values) から Get して 受取った JSON オブジェクトを設定しています。

次は、 about.ts を作成します。

```javascript:about.ts
///<reference path="../app.ts" />

'use strict';

namespace SampleAngularApp.Controllers {
    export class AboutCtrl {

        /**
         * タイトル
         */
        title: string = 'about';

        /**
         * コンストラクタ
         */
        constructor() {

        }
    }
}

angular.module('app')
    .controller('AboutCtrl', SampleAngularApp.Controllers.AboutCtrl);
```

about.ts は ViewModel に 画面に表示するタイトルを設定しているだけです。

## View の作成
View を作成します。

### html ファイルの作成

まずは wwwroot/app フォルダに index.html を作成します。

```html:index.html
<!DOCTYPE html>
<html ng-app="app">
<head>
    <title>ASP.NET Core で AngularJS + Typescript</title>
    <!-- bower -->
    <link href="../bower_components/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
    <!-- styles -->
    <link href="styles/app.css" rel="stylesheet" />
    <link href="styles/main.css" rel="stylesheet" />
    <link href="styles/about.css" rel="stylesheet" />
</head>
<body>

    <!-- ナビゲーションバー -->
    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#/">sampleAngular</a>
            </div>
            <div class="collapse navbar-collapse" id="js-navbar-collapse">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#/">Home</a></li>
                    <li><a ng-href="#/about">About</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <!-- メインコンテンツ -->
    <div class="container">
        <div ng-view=""></div>
    </div>

    <!-- bower -->
    <script src="bower_components/jquery/dist/jquery.js"></script>
    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/angular-route/angular-route.js"></script>
    <!-- scripts -->
    <script src="scripts/app.js"></script>
    <script src="scripts/controllers/main.js"></script>
    <script src="scripts/controllers/about.js"></script>
</body>
</html>
```

とりあえず、ナビゲーションバー内のリンクで遷移する感じです。

次は遷移したときに表示する View のテンプレートを作成していきます。

wwwroot/app/views フォルダに main.html と about.html を作成します。

```html:main.html
<div class="main">
    <h1 class="page-header"><small>{{ main.title }}</small></h1>
    <div><ul><li ng-repeat="value in main.values">{{ value }}</li></ul></div>
</div>
```

```html:about.html
<div class="about">
    <h1 class="page-header"><small>{{ about.title }}</small></h1>
    <div class="jumbotron">
        このサイトについて(以下略<span class="glyphicon glyphicon-heart"></span>)
    </div>
</div>
```

### less ファイルの作成

最後に less ファイルです。

wwwroot/app/styles に以下のファイルを作成していきます。

全体的なスタイルを app.less で扱います。

```css:app.less
body {
    margin-top:60px;
}
```

共通で使う変数やスタイルを _common.less に定義します。

```css:_common.less
.common-header(){
    .page-header {
        margin-top:-5px;
    }
}
```

それぞれの View に対応する less ファイルを作成します。

```css:main.less
@import "_common";

.main {
    .common-header();
}
```

```css:about.less
@import "_common";

.about {
    .common-header();
}
```

以上で Client-side の実装は終わりです。

# おわり
ASP.NET Core で AngularJS + TypeScript してみました。 Web API と AngularJS の組み合わせは Server-side とClient-side で分けて開発できるのでなかなかよいです。