Help us understand the problem. What is going on with this article?

ASP.NET Core + Vue.js (TypeScript)で開発する時の覚書

ASP.NET Core + Vue開発時まとめ

概要

きっとみんなこのスタックで開発したいはずなのに、
個別の情報は散在していてまとまった情報がないのでまとめてみた
(AngularとReactはVSやCore SDKのテンプレートがあるのにVueはなぜかない...)

できるようになること

  • C#(Server side) + TypeScript + Vue(Client)での開発
  • Hot Module Reloadingによるデバッグ時でのフロントエンドコードの修正・リアルタイム反映
  • リリース・デプロイ時のWebpackの設定

この記事で触れないもの

  • Vue CLI : 結局Webpackを触らないとやってられないし、サーバはASP.NET Coreを使えばいいからあまりメリットがない
  • 認証周り : ASP.NET Coreサーバ単体で運用するんだったらASP.NET Core Identity, バックエンドを分けてCORS設定してJWTをVuexに入れるとか考えられるがここでは割愛。

環境

Windows

  • Visual Studio
  • .Net Core SDK
  • Node開発モジュール

Mac, Linux, WSL

  • .Net Core SDK
  • Node.js
  • Visual Studio Code
    • Vetur
    • TS Lint
    • C# Extension

これらがインストール済みであることが前提。
Node, Net Core SDK、VS Codeなどは出来れば最新のものが望ましい。

準備

VSはASP.NET Coreのテンプレから構築。GUIでどれを選んでもいい。
CLIベースのツールで以降は説明。

新規のprojectを作成し、パッケージを追加する

dotnet new webapp --name QiitaArticle
cd QiitaArticle
dotnet add package Microsoft.AspNetCore.SpaServices
dotnet add package Microsoft.Typescript.MSBuild
dotnet add package Microsoft.VisualStudio.Web.BrowserLink
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install --global dotnet-aspnet-codegenerator

Nodeの設定とモジュールのインストール

npm init # デフォルトの設定でOK
npm i vue axios
npm i -D webpack webpack-cli webpack-hot-middleware clean-webpack-plugin aspnet-webpack webpack-dev-middleware
npm i -D typescript ts-loader css-loader vue-loader vue-template-compiler webpack-merge terser-webpack-plugin
npm i -D vue-class-component vue-property-decorator vuex vue-router

各種設定ファイルを配置する

mkdir Frontend

ここに以下を配置していく

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "experimentalDecorators": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "lib": ["dom", "es5"]
  },
  "exclude": [
    "node_modules"
  ]
}

互換性はES5とした。APIの通信でPromiseをメソッドチェーンよりもC#書きにとって馴染み深い async / await記法を使いたいのでlibに追加する。
Vueのコンポーネントの各要素の属性を指定するデコレータは現時点ではExperimentalなのでその旨明記。

vue-shims.d.ts
declare module '*.vue' {
    import Vue from 'vue';
    export default Vue;
}

VueファイルをTSとして読み込むために必要

Webpackの設定

開発と本番で設定ファイルを分ける。共通部分は common にいれる。
HTML Loaderなどは特に不要。

webpack.common.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    entry: { main: './Frontend/index.ts' },
    output: {
        path: path.resolve(__dirname, 'wwwroot'),
        filename: 'js/[name].js',
        publicPath: '/'
    },
    resolve: {
        extensions: ['.ts', '.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {}
            },
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                exclude: /node_modules/,
                options: {
                    appendTsSuffixTo: [/\.vue$/]
                }
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
    ]
};

Devではソースマップを有効にする。

webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './wwwroot',
    }
});

Prodではminifyする。

webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(common, {
    mode: 'production',
    optimization: {
        minimizer: [
            new TerserPlugin({
                terserOptions: { ecma: 5, compress: true,
                    output: { comments: false, beautify: false }
                }
            })
        ]
    }
});

package.jsonを編集して、Webpackのコマンドを呼び分ける

package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+   "build": "webpack --config webpack.dev.js"
+   "release": "webpack --config webpack.prod.js"

  },

本番デプロイ用にcsprojを編集すうる

QiitaArticle.csproj
    <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.2.0" />
  </ItemGroup>


+ <Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
+   <Exec Command="npm i" />
+   <Exec Command="npm run release" />
+   <ItemGroup>
+     <DistFiles Include="wwwroot\**" />
+     <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
+       <RelativePath>%(DistFiles.Identity)</RelativePath>
+       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+     </ResolvedFileToPublish>
+   </ItemGroup>
+ </Target>

</Project>

ASP.NET CoreでHMRを使えるようにする

Sartup.cs
using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.AspNetCore.SpaServices.Webpack;

//(中略)

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
+               app.UseBrowserLink();
+               app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
+               {
+                   HotModuleReplacement = true,
+                   ConfigFile = @"./webpack.dev.js"
+               });
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

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

Fall back routeはVue RouterでClient Side Renderingした際に必ず必要なので入れる。

VueがHello worldできることを確認する

Frontend
├───Components
│   └───HelloWorld.vue
├───index.ts
├───tsconfig.json
└───vue-shims.d.ts
index.ts
import Vue from 'vue'
import HelloComponent from './Components/HelloWorld.vue'

let v : Vue = new Vue({
    el: '#app',
    template: `
    <div>
        Name: <input v-model="name" type="text">
        <hello-component :name="name" :initialEnthusiasm="5" />
    </div>
    `,
    data: { name: 'World' },
    components: { HelloComponent }
});
Components/HelloWorld.vue
<template>
    <div>
        <div class="greeting"> Hello {{ name }} {{ exclamationMarks }}</div>
        <button @click="decrement"> -</button>
        <button @click="increment"> +</button>
     </div>
</template>

<script lang = "ts">
import { Vue, Component, Prop } from 'vue-property-decorator';

@Component
export default class HelloDecorator extends Vue {
    @Prop() name!: string;
    @Prop() initialEnthusiasm!: number;

    enthusiasm = this.initialEnthusiasm;

    increment() {
        this.enthusiasm++;
    }
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    }

    get exclamationMarks(): string {
        return Array(this.enthusiasm + 1).join('!');
    }
}
</script>
Pages/Shared/_Layout.cshtml
    </environment>
    <script src="~/js/site.js" asp-append-version="true"></script>
+   <script src="~/js/main.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
Pages/Index.cshtml
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
+   <div id="app"></div>
+   <div id="api"></div>
</div>
dotnet run

でエラーが出ないことを確認。またDeveloper ToolのConsole上に [HMR] と表示されていれば、
フロントエンドのコードをデバッグ実行中に編集しても変更が反映される。

APIを追加して、コンポーネントから呼び出して表示させる

dotnet aspnet-codegenerator controller -name SampleController --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries
Controllers/SampleCOntroller.cs
using Microsoft.AspNetCore.Mvc;

namespace QiitaArticle.Controllers
{
    [Route("api/sample")]
    [ApiController]
    public class SampleController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return Json(new string[] {"Vue.js", "With", "ASP.NET Core", "Rocks!"});
        }
    }
}
Frontend/Components/CallingApi.vue
<template>
    <div>
        <ul v-for="c in content" :key="c">
            <li>{{c}}</li>
        </ul>
     </div>
</template>

<script lang = "ts">
import { Vue, Component, Prop } from 'vue-property-decorator';
import axios from 'axios';

@Component
export default class CallingApi extends Vue {
    content : string[] = [];
    async mounted() : Promise<void> {
        this.content =  (await axios.get('api/sample')).data;
    }
}
</script>
index.ts
import Vue from 'vue'
import HelloComponent from './Components/HelloWorld.vue'
+ import CallingApi from './Components/CallingApi.vue'

new Vue({
    el: '#app',
    template: `
    <div>
        Name: <input v-model="name" type="text">
        <hello-component :name="name" :initialEnthusiasm="5" />
    </div>
    `,
    data: { name: 'World' },
    components: { HelloComponent }
});
+ new CallingApi({}).$mount('#api');

これでControllerから取り出した内容が表示されるはず

本番デプロイ

環境変数を変えてProductionモードで見てみる。

dotnet publish -c Release
export ASPNETCORE_ENVIRONMENT=Production
cd ./bin/Release/aspnetcoreapp2.0/publish
dotnet QiitaArticle.dll

これで起動した後、F12を押してConsoleを見てみると、minifyされたJSが読み込まれていて、 HMR が消えている。
実際はこれをサーバに置けばOK.

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした