はじめての Nuxt.js

Nuxt はずっと前から気にはなってて、でも「今回は SSR しないからいいや」とか「今回は静的サイトジェネレーターで作るからいいや」とか思って避けてきたけど、なんか最近 Nuxt 使うって話を 3 回くらい聞いたので、ちょっと使ってみて構成とか眺めてみようと思う

先に使ってみた感想を述べておくと

- [Nuxt の公式ドキュメント](https://ja.nuxtjs.org/guide)がちゃんと作られているので、これだけ見れば大体解決した
- 後述してますが TypeScript とか Vuex とかとても簡単に導入できるので感動した
- webpack 周りが隠蔽されてる
  - 前に VueCLI v2 使ってみた時はけっこうビルド周りがむき出しになってた気がする（VueCLI v3 ではこの辺解決されてるっぽい）
- Vue で SSR する時に Nuxt を使うという印象を持っていたが、SSR モードか SPA モードか選べたので考えが違った
- フロントエンドチームが多いチームであれば、Angular などのガッチリ系フレームワークも検討しようと思っていたが、Vue が好きなら Nuxt でもいいかもしれない
  - TypeScript のサポートも昔よりはかなり良くなってて、Vue v3 がリリース（今年リリースされる？）されたら Vue 自体が TypeScript で作り直されるので、さらに Vue + Nuxt + TypeScript 環境が良くなっていくのではないかと思う

今回作業したリポジトリは以下
https://github.com/kurosame/event-search

## インストール

[create-nuxt-app](https://github.com/nuxt/create-nuxt-app)を使うと良さそう
npx でいけるとかすばらしい

```
npx create-nuxt-app event-search
```

プロジェクト名は GitHub のリポジトリ名にした

```
? Project name event-search
? Project description My groundbreaking Nuxt.js project
? Use a custom server framework express
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier, Axios
? Use a custom UI framework vuetify
? Use a custom test framework jest
? Choose rendering mode Universal
? Author name kurosame
? Choose a package manager yarn
```

とりあえず全部入れといたが、以下に該当する場合はインストールから除外してもいいと思う
（でも環境を CLI ツールで管理する時点で環境周りをメンテナンスすることは基本的に無いと思うので、全部入れてしまっても問題無いと思う）

- SSR しない

```
? Use a custom server framework
None
? Choose rendering mode
SPA
```

- モバイル対応しない

```
? Choose features to install
Progressive Web App (PWA) Supportを選択しない
```

- HTTP リクエストしない（ほぼ無いと思うが）

```
? Choose features to install
Axiosを選択しない
```
このAxiosとは、axiosを拡張した[axios-module](https://github.com/nuxt-community/axios-module)のことである

- デザインは全部自前

```
? Use a custom UI framework
None
```

サーバーの Node フレームワークはあまり詳しくないが、特に今使っているのが無ければ Express か Koa あたりを選んでおけば良いと思う
Vuetify は 1 年くらい使ってるが、かなり完成度高い UI フレームワークと思っているので、入れた
Linter や Prettier や Jest などのテストフレームワークは必須で入れておいた方が良い

## 実行

package.json を開くと、いくつかスクリプトが設定してあるが、とりあえず以下を実行

```
yarn dev
```

マシン環境にもよるが、まあまあ時間はかかる
![スクリーンショット 2019-05-27 13.13.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/b2e219c5-b975-e41f-e97b-733534f6325b.png)

2 回目以降は`node_modules/.cache`の下に`babel-loader`のキャッシュを作ってる影響からか、少し速くなっている
![スクリーンショット 2019-05-27 13.13.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/13dc512c-98e3-abbd-2684-3534d81e8fa1.png)

でもホットリローディングをサポートしているので、1 回起動すれば遅さは気にならない
[nodemon](https://github.com/remy/nodemon)を使って`./server`ディレクトリ内のファイルを監視してサーバの再起動を行っている

画面が表示されたら HTML ソースを見てみると、`server-rendered="true"`及び HTML の各要素がそのままレンダリングされていると思う
ちゃんと SSR されていることが分かる
ちなみに SPA モードであれば、JS が実行されてから画面がレンダリングされるので、読み込む JS ファイルのみが指定してあり、HTML はスタイルなどを除けばほとんど空である

また、実行すると`.nuxt`というディレクトリができており、サーバを立ち上げた時に読み込ませるコンテンツが格納されている
`./server/index.js`を見てみると、Nuxt のインスタンスを作成して、Promise を返す`nuxt.render`を Express に Middleware としてセットしている

しばらく開発で使うのは、dev スクリプトだけで良さそう

以下の他のスクリプトはある程度 Nuxt でアプリケーション作った後にちゃんと使ってみようと思う

- build
  - minify して`.nuxt/dist/`に格納
- start
  - production モードでサーバを立ち上げる
- generate
  - Vue を静的コンテンツとしてビルドして`dist/`に格納してくれる
  - Netlify や Firebase などでそのままホスティングできる（はず）
  - VuePress 使ったことがあれば、それ

## 使ってみる

ちょっとだけ使ってみてやったことや思ったことを書いてみようと思う

### TypeScript を使う

```
yarn add -D @nuxt/typescript ts-node
```

`tsconfig.json`があれば`nuxt`コマンドで勝手にデフォルト値を書いてくれるらしい

```
touch tsconfig.json
npx nuxt
```

先程インストールした`ts-node`は Node で TS をトランスパイルせずに実行できるツールでこれが無いと`npx nuxt`が動かない

これで導入は完了

後は、拡張子を`.js`から`.ts`に変えて中身を書き換えていく
以下は`server/index.js`を TS に修正している例

```ts:server/index.ts
import NuxtConfiguration from '@nuxt/config'
...
// Import and Set Nuxt.js options
const config: NuxtConfiguration = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
...
```

TS 導入前は上記の`config.dev`が any 型だったが、導入後はちゃんと型定義の boolean 型を参照している

package.json も忘れずに修正

```json:package.json
"scripts": {
-    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+    "dev": "cross-env NODE_ENV=development nodemon server/index.ts --watch server",
-    "start": "cross-env NODE_ENV=production node server/index.js",
+    "start": "cross-env NODE_ENV=production ts-node server/index.ts",
}
```

Vue コンポーネントの TS 化についてはドキュメントでは`vue-property-decorator`の使用を薦めている

```
yarn add vue-property-decorator
```

`layouts/default.vue`を例に修正すると

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

@Component
class Default extends Vue {
  clipped: boolean = false
  drawer: boolean = false
  fixed: boolean = false
  items: { icon: string; title: string; to: string }[] = [
    {
      icon: 'apps',
      title: 'Welcome',
      to: '/'
    },
    {
      icon: 'bubble_chart',
      title: 'Inspire',
      to: '/inspire'
    }
  ]
  miniVariant: boolean = false
  right: boolean = true
  rightDrawer: boolean = false
  title: string = 'Vuetify.js'
}

export default Default
</script>
```

ちなみに`vue-property-decorator`を使わなくても書ける

```vue
<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data(): {
    clipped: boolean
    drawer: boolean
    fixed: boolean
    ...
  } {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      ...
    }
  }
})
</script>
```

### Vuex を使う

Nuxt をインストールした時にルート直下に store ディレクトリができており、既に Vuex 入ってる感があるが、中身は空である
Nuxt は store ディレクトリが存在すれば Vuex をインポートしてくれて、store をルートインスタンスに流してくれるらしい

なので

- Vuex を手動で入れる必要がない
- どのコンポーネントからでも`this.$store`で参照できる

また、モジュールモードとクラシックモードの 2 つのモードがあるが、クラシックモードは廃止予定らしい
肥大化してくるとモジュールごとに State が参照できた方が良いので、モジュールモードで良さそう

Nuxt 側でモジュールモードとクラシックモードのどちらで Vuex インスタンスが作られるかの判定は
`store/index.(js|ts)`が存在して、Store インスタンスをエクスポートしていれば、クラシックモード
それ以外はモジュールモード

モジュールモードで`store/index.(js|ts)`という名前で作った場合、ルートモジュールという扱いで Store インスタンスの直下に State が存在する
`store/モジュール名.(js|ts)`という名前で作った場合、名前付きモジュールとして Store インスタンスの modules プロパティ配下に State が存在する

それぞれの State を参照する場合

- ルートモジュールは`this.$store.state.counter`で参照できる
- ルート以外は`this.$store.state.todos.list`で参照できる
  - 上記は`store/todos.(js|ts)`というファイルを作った場合

ちなみに私は上記のルートモジュールと呼ばれてる Store インスタンスの直下に State を置くのは 1 度もやったことない

以下のファイルを store ディレクトリ配下に追加

```ts:store/sample.ts
interface IState {
  counter: number
}

export const state: IState = {
  counter: 123
}
```

簡単すぎる例だが、ファイルを追加するだけで Store に counter が保持できる

そして以下のように参照する

```vue:適当なVue
<script>
export default {
  mounted() {
    console.log(this.$store.state.sample.counter) // 123
  }
}
</script>
```

また、Vuex のヘルパー関数も使える

```vue:適当なVue
<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      sample: 'sample'
    })
  },
  mounted() {
    console.log(this.sample.counter) // 123
  }
}
</script>
```

### Linter とコードフォーマッターを使う

TS を使っているが、TSLint は[非推奨](https://github.com/palantir/tslint/issues/4534)となるらしいので、ESLint の TS パーサーを使う
`.eslintrc.js`を見る限り ES(JS)だけの Linter で良ければ、Nuxt をインストールした時点で出来てるので、後は rules や extends でルールを追加するだけで良さそう

`package.json`を見ると`eslint-plugin-prettier`と`eslint-config-prettier`が依存パッケージとしてインストールされており、`.eslintrc.js`の extends に prettier が設定してあるので、ESLint と Prettier の競合は気にしなくて良さそう

TS 用にLinterを使う場合の修正箇所は以下

```
yarn add -D @typescript-eslint/eslint-plugin
```

```js:.eslintrc.js
parserOptions: {
-    parser: 'babel-eslint'
+    parser: '@typescript-eslint/parser'
},

-  plugins: ['prettier'],
+  plugins: ['@typescript-eslint', 'prettier'],
```

```json:package.json
"scripts": {
-    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
+    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
}
```

また、ESLint のルールだと以下のような import は誤検知して、「no-unused-vars」を出してしまう

```
import NuxtConfiguration from '@nuxt/config'
```

この場合、以下のように ESLint のルールは握りつぶして、`@typescript-eslint`の方のルールを有効化する必要があるらしい
[こちらの記事](https://teppeis.hatenablog.com/entry/2019/02/typescript-eslint)に書いてありますが、いくつかこのような誤検知するルールがあるらしいので、その都度同様の対応が必要

```js:.eslintrc.js
rules: {
  'no-unused-vars': 'off',
  '@typescript-eslint/no-unused-vars': 'error'
}
```

Airbnb の ESLint ルールを入れてみる

```
yarn add -D eslint-config-airbnb-base
```

```js:.eslintrc.js
extends: [
  'airbnb-base',
...
]
```

いい感じに動いているが、Vue とか Vuex は Nuxt に直接依存しており、こちらで`package.json`に書く必要が無いため、以下のような Lint エラーが出てる

```
.../event-search/pages/index.vue
  63:1  error  'vuex' should be listed in the project's dependencies. Run 'npm i -S vuex' to add it  import/no-extraneous-dependencies

.../event-search/plugins/vuetify.js
  1:1  error  'vue' should be listed in the project's dependencies. Run 'npm i -S vue' to add it  import/no-extraneous-dependencies
```

上記は以下のように対応可能

```js:.eslintrc.js
settings: {
  'import/core-modules': ['vue', 'vuex']
},
```

他にも Nuxt と ESLint で競合するルールはありそう

### vue-router を使う

Nuxt ではエントリーポイントとなる JS などで routes を書いて`new VueRouter`をやる必要はない
pages ディレクトリ配下にルーターから直接呼ばれるコンポーネントを配置するだけで良い

以下の 2 つを配置

```vue:pages/sample/index.vue
<template>
  <span>サンプルインデックス！</span>
</template>
```

```vue:pages/sample/test.vue
<template>
  <span>サンプルテスト！</span>
</template>
```

それぞれ以下のようにしてルーティングさせる

```vue:適当なVue
<nuxt-link to="/sample">Go sample index</nuxt-link>
<nuxt-link to="/sample/test">Go sample test</nuxt-link>
```

ファイル名が`index.vue`であれば、`ディレクトリ名`が path になり、それ以外のファイル名であれば、`ディレクトリ名/ファイル名`が path になる
先程の Vuex とルールが似ている

パラメータ付きの動的ルーティングもファイル名もしくはディレクトリ名の先頭にアンダースコアを付けることで実現できるらしい

nuxt-link というコンポーネントは router-link を extends したコンポーネントとなっており、nuxt-link を使わずに今まで通り`<router-link>`を使って実装することも可能
ただし、nuxt-link を使うと画面表示領域にリンクがあれば、そのリンク先のコンテンツを先読みしてくれるので、遷移が速くなる

試しに、以下のコードを適当な Vue の下の方に配置して、ファーストビューから見えないようにして、スクロールしてリンクを表示させると`index.vue`だけプリフェッチされることが分かる
Chrome DevTool の Network タブで確認すると分かりやすい

```vue:適当なVue
<nuxt-link to="/sample">Go sample index</nuxt-link>
<router-link to="/sample/test">Go sample test</router-link>
```

### その他

#### 選択した UI フレームワークでサンプルコードが作られている

私は Vuetify を選んだのだが、割としっかりサンプルコードが Vuetify で作られてて、全部の UI フレームワーク分作られてると思うとちゃんとしてるなって思った

#### テストのサンプルがほぼ無い

Logo コンポーネントの 1 ケースしかテストのサンプルコードが無い
E2E のサンプルコードは無い

でも最近は Vue のテスト周りのエコシステムが充実してきて、ドキュメントも豊富なので、問題ないと思う

#### Nuxt のアップデートは簡単にできるのだろうか

Angular みたいなマイグレーション機能はたぶん無いと思うが、Nuxt とか CLI 系のツールを使っているとアップデートが楽にできるとかなりありがたい
簡単にネットで調べた限りは Nuxt を最新にして出たエラーを潰してるっぽい

## さいごに

主にツールの導入になってしまい、まだコードはほとんど書いてないのですが、これから ToDo アプリ程度のものは作ってみて、Nuxt を学んでみようかと思います
続きは別記事でまた書こうかなと思います

フロントエンドは全部 1 から作る派なのですが、たまに自動で環境作ってくれる系ツールを使うとそのお手本のような構成に勉強になることがあります
また、今後フロントエンドの環境を作る上で活かせることもあると思うので、試しに使ってみるのもありかなと思いました