OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

Vue.js × webpack 3 × TypeScript 環境構築

タイトルのとおり。VueとwebpackとTypeScript。
似たような情報はいくらでもヒットするし、それらの単なる焼き直しでしかないのだけど、結局いちいち自分用に焼き直しておかないとわからなくなってしまう。
参考にさせていただいたページリストが最後にあります。

前提とかバージョンとか

vue-cliを使います。

たぶん現在は Vue CLI 3 (@vue/cli) を使うべきだが、いろいろサンプルが見つかったのがVue CLI 2 (vue-cli) だったので、今回はこちらを使う。

フロントエンド界隈の情報は、しばらく調査を進めてから実はそれはもう旧いバージョンでしたということがよくある。

したがって、webpackも4ではなく3を使うことになります。

  • vue-cli: v2.9.6
  • webpack: 3.6.0
  • TypeScript: 3.0.1

なお、文中のコマンド例はyarnを使いますが、npmの方は適宜読み替えてください。

あと本内容の検証はWindows 10 環境でやってます。

エディタはVisual Studio Codeを使用。

vue-cliでプロジェクトを初期化

vue-cliをグローバルインストール。

>yarn global add vue-cli  

# コマンドが使えることを確認  
>vue --version  
2.9.6  

自分の環境だと、yarnのバグなのか、globalインストールしたパッケージに対するPATHが自動で設定されなかった。

その場合は自分でPATHを通す。
Windows 10 だと C:\Users\ユーザー名\AppData\Local\Yarn\Data\global\node_modules.bin かな。

その後、プロジェクトフォルダを作って、webpack用のテンプレートでプロジェクトを生成する。

>mkdir vue-webpack-ts-sample  

>cd vue-webpack-ts-sample  

>vue init webpack  

? Generate project in current directory? Yes  
? Project name vue-webpack-ts-sample  
? Project description A Vue.js project  
? Author YOUR_NAME <EMAIL_ADDRESS>  
? Vue build standalone  
? Install vue-router? Yes  
? Use ESLint to lint your code? No  
? Set up unit tests No  
? Setup e2e tests with Nightwatch? No  
? Should we run `npm install` for you after the project has been created? (recom  
mended)  
  Yes, use NPM  
> Yes, use Yarn  
  No, I will handle that myself  

initコマンドを打ってから、いくつか質問に答えます。

  • Vue build:「Runtime + Compiler」を選択
  • Install vue-router: SPAだと使うことになるでしょう。入れておく
  • Use ESLint: いったんNo。TSLintを使うことになるかな
  • Set up unit tests: いったんNo
  • Setup e2e tests: No。Nightwatch使ったことない。

いくつかWarningが出ました。ネットワークエラーはさておき。

[1/5] Validating package.json...  
[2/5] Resolving packages...  
warning autoprefixer > browserslist@2.11.3: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools.  
warning css-loader > cssnano > autoprefixer > browserslist@1.7.7: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools.  
warning css-loader > cssnano > postcss-merge-rules > browserslist@1.7.7: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools.  
warning css-loader > cssnano > postcss-merge-rules > caniuse-api > browserslist@1.7.7: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools.  
info There appears to be trouble with your network connection. Retrying...  
warning webpack-bundle-analyzer > bfj-node4@5.3.1: Switch to the `bfj` package for fixes and new features!  
[3/5] Fetching packages...  
info fsevents@1.2.4: The platform "win32" is incompatible with this module.  
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.  
[4/5] Linking dependencies...  
[5/5] Building fresh packages...  
success Saved lockfile.  

開発モードで起動してみる。

>yarn dev  

Your application is running here: http://localhost:8080 と出るので、このURLにブラウザでアクセス。

表示されました。

エディタ設定

おせっかいにも .editorconfig ファイルが作られているので、気にくわない設定があれば変更する。  
とりあえず4タブに変更した。

root = true  

[*]  
charset = utf-8  
indent_style = space  
indent_size = 4  
end_of_line = lf  
insert_final_newline = true  
trim_trailing_whitespace = true  

それとVSCodeの拡張から Veturを入れておく。

TypeScriptの導入

ビルド設定

TypeScriptとts-loaderをローカルインストール。

>yarn add --dev typescript ts-loader@3.5.0  

このとき、ts-loaderはwebpack 3に対応している v3.5.0を入れる必要あり。

v4.4以降では「You may be using an old version of webpack; please check you're using at least version 4」という親切なエラーメッセージがでる。

それ以前のv4版では「Cannot read property 'afterCompile' of undefined」というエラーが吐かれるようだ。

tsconfig.jsonを作成。

{  
  "compilerOptions": {  
    "target": "es5",  
    "module": "commonjs",  
    "strict": true,  
    "esModuleInterop": true,  
    "allowSyntheticDefaultImports": true  
  },  
  "include": [  
    "./src/**/*.ts"  
  ]  
}  

[※追記] "strictFunctionTypes": false がないとエラーになるかも

次に build/webpack.base.conf.js を編集する。

entryの拡張子をtsに。

entry: {  
  app: './src/main.ts'  
},  

resolveの拡張子にtsを追加。

resolve: {  
  extensions: ['.js', '.ts', '.vue', '.json'],  
  alias: {  
    'vue$': 'vue/dist/vue.esm.js',  
    '@': resolve('src'),  
  }  
},  

module.rulesにts-loaderを追加。

{  
  test: /\.ts$/,  
  loader: 'ts-loader',  
  exclude: /node_modules/,  
  options: {  
    appendTsSuffixTo: [/\.vue$/]  
  }  
},  

TypeScriptを組み込む

Vueの型定義はnpmのvueパッケージに同梱されている。

拡張子 .vue のファイルをモジュールと見なしてimportできるように、以下の型宣言を作っておく必要がある。

ここでは src/types/common.d.ts として作成。

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

続いて、main.js, router/index.js の拡張子を .ts に変更しておく。

このとき、.vueをインポートしている行がエラーとなって、declareしたのに!?とハマったんだけど、import元の指定に拡張子をつけてやらなきゃいけないという問題でした。

import App from './App.vue'  

.vueの単一コンポーネントにも型情報を入れてやる。

App.vue内のscriptタグを以下のように変更。

<script lang="ts">  
import Vue from "vue";  

export default Vue.extend({  
  name: 'App'  
});  
</script>  

components/HelloWorld.vueも同様。

<script lang="ts">  
import Vue from "vue";  

export default Vue.extend({  
  name: 'HelloWorld',  
  data () {  
    return {  
      msg: 'Welcome to Your Vue.js App'  
    }  
  }  
})  
</script>  

再び yarn dev で画面が表示できればOK。

classコンポーネント

TypeScriptを使う場合は、classコンポーネントスタイルで書いた方が型の恩恵を受けやすいらしい。

それってつまり単一コンポーネント(.vue)を使わないってこと?と思ったんだが、単一コンポーネント内のscriptの部分だけclassコンポーネントスタイルにするでも良いのかな。

とりあえずそんな感じにしてみる。

まずは以下2つをインストール。

>yarn add --dev vue-class-component vue-property-decorator  

tsconfig.jsonのcompilerOptionsに以下を追加。

  "experimentalDecorators": true  

App.vueのscriptを以下のように書き換える。

<script lang="ts">  
import Vue from "vue";  
import Component from "vue-class-component";  

@Component  
export default class App extends Vue {  
}  
</script>  

続いてHelloWorld.vue。

そのままだとつまらないので、ちょっと動きをつけてみる。

<template>  
  <div class="hello">  
    <h1>Hello, {{count}} times.</h1>  
    <button @click="increment">Hello!</button>  
  </div>  
</template>  

<script lang="ts">  
import Vue from "vue";  
import Component from "vue-class-component";  

@Component  
export default class HelloWorld extends Vue {  

    public count = 0;  

    public increment() {  
        this.count++;  
    }  
}  
</script>  

ボタンをクリックする度カウントアップされていけば成功。

上記例ではvue-property-decoratorのほうは使用していないが、こちらはpropsをクラスのプロパティとして書くために使用する。

単一コンポーネントを使用しない

おまけ。
.vueを使わず、コンポーネントも.tsファイルとして書く場合。

※以下、一部未解決のままです

単一コンポーネントでない場合はscoped cssが使えない(たぶん)ので、とりあえず単純に各styleを1つの外部CSSにまとめてしまう。

App.vue, components/HelloWorld.vue内のstyleタグの中身をまとめて、style.cssを作成。

#app {  
    font-family: 'Avenir', Helvetica, Arial, sans-serif;  
    -webkit-font-smoothing: antialiased;  
    -moz-osx-font-smoothing: grayscale;  
    text-align: center;  
    color: #2c3e50;  
    margin-top: 60px;  
}  

h1, h2 {  
    font-weight: normal;  
}  
ul {  
    list-style-type: none;  
    padding: 0;  
}  
li {  
    display: inline-block;  
    margin: 0 10px;  
}  
a {  
    color: #42b983;  
}  

静的ファイルは staticフォルダの中に入れないとだめっぽいので、static/style.css とする。

index.htmlでこれを読み込む。

<!DOCTYPE html>  
<html>  
  <head>  
    <meta charset="utf-8">  
    <meta name="viewport" content="width=device-width,initial-scale=1.0">  
    <title>vue-webpack-ts-sample</title>  
    <link rel="stylesheet" type="text/css" media="screen" href="static/style.css" />  
  </head>  
  <body>  
    <div id="app"></div>  
    <!-- built files will be auto injected -->  
  </body>  
</html>  

最初に作ったsrc/types/common.d.ts は不要になるので削除してよい。

main.ts, router/index.ts内の import箇所から、再び .vue拡張子を削除する。

import App from './App'  

router/index.tsでHelloWorldをimportしているほうは、なぜか @ のエイリアス(webpack.base.conf.jsで定義されている)がエラーになるようだったので、原因はわからないがとりあえずエイリアスを使わない形で書き換える。

import HelloWorld from '../components/HelloWorld'  

App.vue を App.ts に変更し、中身も以下のように書き換える。

import Vue from "vue";  
import Component from "vue-class-component";  

@Component({  
    "template": `  
        <div id="app">  
            <img src="./assets/logo.png">  
            <router-view/>  
        </div>  
    `  
})  
export default class App extends Vue {  
}  

HelloWorld.vueも .tsに。

import Vue from "vue";  
import Component from "vue-class-component";  

@Component({  
    "template": `  
        <div class="hello">  
            <h1>Hello, {{count}} times.</h1>  
            <button @click="increment">Hello!</button>  
        </div>  
    `  
})  
export default class HelloWorld extends Vue {  

    public count = 0;  

    public increment() {  
        this.count++;  
    }  
}  

ただしこれだと、ロゴの画像が表示されない。

imgのsrc属性をそのまま書くんじゃなくてバインドにする必要があるか?

未確認だけど、まあ当面こんな書き方もしないだろうってことで、保留。

参考資料