Visual StudioでASP.NET Core + Vue.js + TypeScriptの環境構築してみた

#追記（2018.12.26）
特にこだわりがないのであればVisual StudioでVue.jsの環境を整えるメリットはないです。
.vueファイルもうまく読み込まないですし。
[vuejs/vue-cli](https://github.com/vuejs/vue-cli)や[nuxt/create-nuxt-app](https://github.com/nuxt/create-nuxt-app)で環境構築して、Vue.js部分はVisual Studio Codeで開発するのがベストだと思います。一押しは[Nuxt.js](https://github.com/nuxt/nuxt.js)と[Buefy](https://buefy.github.io/)です。楽ちんです。



仮に頑張ってWebpackの設定をおこなったとして、
その人がプロジェクトから離れてしまって誰も構成部分をさわれない。でもpackageのバージョンは上がっていく…脆弱性が…などの属人化も未然に防げますね！

#はじめに
次のプロジェクトで.Net FrameworkにVue.jsを組み込むらしく、環境構築周りを調べました。
ASP.NET CoreとVue.jsの環境構築の手順を残します。
ちなみにCoreを使用したのは私の趣味です。

#ソースコード
GitHubで公開しています。
https://github.com/ishiyama0530/TsVueTemplate

#使わなかったもの
BabelはTypeScriptコンパイラがこなしてくれるので使ってません。
また、Gulpも必要ないだろうと思い使いませんでした。
>[[意訳]私がGulpとGruntを手放した理由](https://qiita.com/chuck0523/items/dafdbd19c12efd40e2de)

#Visual Studioの設定（外部Webツール）
Visual StudioからどのNode.jsを使うかっていう設定。グローバルにインストールされているNode.jsを指定したいので「$(PATH)」を一番上しておきます。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/62209/88d97e37-6793-50c1-ddc0-d41ee1d904e8.png)

#構成
###プロジェクト構成
![10.png](https://qiita-image-store.s3.amazonaws.com/0/62209/d1f69187-7a2f-e84b-0d21-33cc2dcdd1b9.png)

今回はwebpackでTypeScriptのコンパイルを行うので、Visual StudioによるTypeScriptのコンパイルは不要です。無効にしておきましょう。

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <TypeScriptCompileBlocked>True</TypeScriptCompileBlocked> //←これを追加
  </PropertyGroup>

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

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.3" />
  </ItemGroup>

  <ItemGroup>
    <None Update="ClientApp\vue.d.js">
      <DependentUpon>vue.d.ts</DependentUpon>
    </None>
    <None Update="ClientApp\vue.d.js.map">
      <DependentUpon>vue.d.js</DependentUpon>
    </None>
  </ItemGroup>

</Project>
```

###package.json
TypeScriptとwebpackもグローバルにインストールしたくないのでpackage.jsonで管理します。最近のnpmはnpxコマンドでnode_modules内を直接実行できるので便利。

```package.json
{
  "name": "tsvuetemplate",
  "version": "1.0.0",
  "devDependencies": {
    "css-loader": "^0.28.11", //webpackでCSSファイル間の依存関係の解決
    "style-loader": "^0.21.0", //webpackでstyleタグの生成
    "vue-loader": "~14.2.2", //webpackでvueをコンパイル
    "ts-loader": "^4.2.0", //webpackでtypescriptをコンパイル
    "typescript": "^2.8.3",
    "uglifyjs-webpack-plugin": "^1.2.5", //webpackでminifyをするため
    "vue-template-compiler": "^2.5.16", //vueのコンパイルに必要っぽい
    "webpack": "^4.6.0",
    "webpack-cli": "^2.1.2", //最新のwebpackではwebpack-cliが含まれなくなったらしく、これも入れないと怒られる
    "mkdirp": "^0.5.1", //windowsでmkdirpコマンドを使用するため
    "rimraf": "^2.6.2" //windowsでrimrafコマンドを使用するため
  },
  "dependencies": {
    "vue": "^2.5.16",
    "vue-class-component": "^6.2.0", //vueをクラス形式で記述したかったため
    "vue-property-decorator": "^6.0.0" //vueをクラス形式で記述するときに使う
  },
  "scripts": {
    "clean": "rimraf ./wwwroot/js", //webpackの出力フォルダを削除
    "build": "webpack -d", //webpackを開発モードでビルド
    "watch": "webpack --watch -d" //webpackのwatchを開発モードで実行
  },
  "-vs-binding": {
    "AfterBuild": [
      "build" //visualstudioのビルドにフック
    ],
    "Clean": [
      "clean" //visualstudioのクリーンにフック
    ],
    "ProjectOpened": [
      "watch" //visualstudioのプロジェクトオープン時にフック
    ]
  }
}
```

[NPM task Runner](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.NPMTaskRunner)を入れるとvs-bindingはGUIから設定ででます。
![2.png](https://qiita-image-store.s3.amazonaws.com/0/62209/96700aa1-b9ca-ace3-8825-6bc34dd3fdcf.png)


###tsconfig.json

```tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "sourceMap": true,
    "strict": false, //strictをtrueにするのはまだ早そう
    "experimentalDecorators": true, //Decoratorを使うため
    "emitDecoratorMetadata": true, //Decoratorを使うため
    "module": "es2015",
    "moduleResolution": "node",
    "lib": [ "es5", "es2015.promise", "dom", "scripthost" ] // async/awaitを使うための
  },
  "include": [
    "./ClientApp/**/*.ts"
  ]
}
```

ソリューション内でtsconfigが見つかればプロジェクトファイルのTypeScriptのビルド設定は無効になります。
![3.png](https://qiita-image-store.s3.amazonaws.com/0/62209/9c978bc7-ee03-587d-05bd-07c45a29910c.png)

###webpack.config.js

```webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = (env, argv) => {
    const config = {
        entry: { 'index': './ClientApp/index.ts' },
        output: {
            path: path.resolve(__dirname, 'wwwroot/js'), //bundleファイルの出力先
            filename: '[name].bundle.js' //[name]はentryの'index'と対応（index.bundle.js）
        },
        module: {
            rules: [
                {
                    test: /\.vue$/, //vueファイル
                    loader: 'vue-loader',
                    options: {
                        loaders: {
                            'scss': 'vue-style-loader!css-loader!sass-loader',
                            'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
                        }
                    }
                },
                {
                    test: /\.ts$/, //tsファイル
                    loader: 'ts-loader',
                    exclude: /node_modules/,
                    options: {
                        appendTsSuffixTo: [/\.vue$/]
                    }
                },
                {
                    test: /\.css$/, //cssファイル
                    use: [
                        { loader: "style-loader" },
                        { loader: "css-loader" }
                    ]
                }
            ]
        },
        resolve: {
            extensions: ['.ts', '.js', '.vue', '.json'],
            alias: {
                'vue$': 'vue/dist/vue.esm.js' //vue$
            }
        }
    };
    
    if (argv.mode !== 'production') {
        config.devtool = '#eval-source-map'; //これがchromeでtsのデバッグするのに良さそう
    }

    if (argv.mode === 'production') {
        // minifyする
        config.optimization = {
            minimizer: [
                new UglifyJSPlugin({
                    uglifyOptions: {
                        compress: {
                            drop_console: true
                        }
                    }
                })
            ]
        };
    }

    return config;
};
```
webpackは'npx webpack -d'でdevelopment、'npx webpack -p'でproductionモードでコンパイルされます。

###vueの型定義
tsconfigのincludeのPathに以下のファイルを用意してvueファイルを認識させます。

```vue.d.ts
declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}
```

###vue周り
```Hello.vue
<script src="./Hello.vue.ts" lang="ts"></script>
<style src="./Hello.vue.css"></style>

<template>
    <section class="section">
        <div class="container">
            <h1 class="title">
                {{exampleProperty}}
            </h1>
        </div>
    </section>
</template>
```
```Hello.vue.ts
import Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'

@Component
export default class App extends Vue {
    @Prop({ default: 'Hello Ts Vue Template.' })
    exampleProperty: string
}
```
```Hello.vue.css
h1 {
    color: orange !important;
}
```
```index.ts
import Vue from 'vue'
import Hello from './Hello.vue'

new Vue({
    el: '#app-root',
    render: h => h(Hello)
})
```
#index.cshtml
index.cshtmlはこんな感じです。
CSS書きたくないので[bulma](https://github.com/jgthms/bulma)を使用しています。

```index.cshtml
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello Bulma!</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script>
</head>
<body>
    <div id="app-root">loading</div>

    <script src="~/js/index.bundle.js"></script>

    @*https://cdnjs.com/libraries/bulma*@
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">

    @*https://fontawesome.com/*@
    <script defer src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script>
</body>
</html>
```
#いざ実行！
npm インストール後、Visual Studioの実行ボタン（IIS Express）からローカルサーバーを起動します。
![5.png](https://qiita-image-store.s3.amazonaws.com/0/62209/63101962-a1bb-c79d-a136-29d3ab81c360.png)

Visual Studioのビルドにフックされたnpmのbuildスクリプトが実行されwwwroot/js配下にindex.bundle.jsが作成されました。

画面はこんな感じ。
![6.png](https://qiita-image-store.s3.amazonaws.com/0/62209/5ece7b85-b5a7-164e-2895-337b66822279.png)

フォントカラーを変更してみます。

```Hello.vue.css
h1 {
    color: red !important;
}
```

![7.png](https://qiita-image-store.s3.amazonaws.com/0/62209/21053abc-cf8f-26c9-395d-e53fdeb17b5e.png)


Visual Studioのプロジェクト読み込み時にフックされているnpmのwatchスクリプトが実行されていれば、自動でコンパイルかけてくれるのでサーバー再起動の必要はないです。
変更が反映されない場合はwatchが動いていないのでVisual Studioの再起動か、タスクランナーエクスプローラーからwatchを起動させてください。

![8.png](https://qiita-image-store.s3.amazonaws.com/0/62209/e7d1b342-36a4-ebc7-205a-bf4e0facd220.png)

Chromeのブレイクポイントも止まります。

#課題
Visual StudioからASP.NET Core + Vue.js + TypeScriptで実行することができました。
個人的にはSFC（single-file components）なら１つのvueファイルにtsもstyleも書きたいのですが、Visual Studioのエディターがまだ対応していないので、それぞれを別ファイルに分け、パスを読み込むことに。
また、対応するvueファイルとtsファイルのウィンドウを同時に開いている場合プロジェクトの指定が「その他」になってしまい、うまくファイルが読み込まれません。

![9.png](https://qiita-image-store.s3.amazonaws.com/0/62209/dc33a863-a29f-03cd-7547-18b491f216ff.png)

#感想
今までVisual Studio様に頼ってきてこの手のツールはほとんど使用したことがありませんでした。なのでかなり手探り状態の状態です。もっとよい方法などがあったらぜひ教えてください！