Nuxt.jsの本格導入で遠回りしないためのTips v1.1

本記事は[Vue meetup #5](https://vuejs-meetup.connpass.com/event/65442/)で発表させていただいた「Nuxt.js本格導入で遠回りしないためのTips」のスライド発表の内容をQiita向けの記事としてまとめ、内容を一部追加したものです。これからNuxt.jsを本格的に使ってみようという方の助力になれば幸いです。

本記事では、そこそこな規模の本格的なアプリケーションにNuxt.jsを導入する場合に多くの方が直面するであろう問題や、知っていると助かりそうなことを紹介します。

## Tips 1. ドキュメントを読もう

本格的なウェブアプリケーションをNuxt.jsで作ろうとしているならば、VueとNuxtのドキュメントを先にしっかり読みましょう。VueやNuxtは使い始めるのに多くのことを知る必要がなく、少しずつ利用の幅を拡げていけます。これはVueやNuxtの良いところです。しかし、本格的なウェブアプリケーションを構築する場合、結局のところそのほとんどの機能を使うことになります。最初にVueとNuxtの機能を俯瞰しざっくり把握しておくことで、再発明をしたりIssueやコードを探し回るといった無駄を減らすことができます。
ガイドだけでなくFAQも最初に読んだ方が良いでしょう。開発を進めるうちに確実に発生する疑問や問題が載っています。

## Tips 2. チェックしておきたいページ

 - nuxt-community/awesome-nuxt

https://github.com/nuxt-community/awesome-nuxt
テンプレートやサンプルなどの一覧

 - nuxt-community/modules

https://github.com/nuxt-community/modules
便利な小粒のプラグインがたくさんあります。
便利なライブラリへの橋渡し的なものがほとんど。敢えて自分で書く必要がなければ使った方が楽です。

このなかでよく使いそうなプラグインの一覧です。

 - axios
 - pwa
 - analytics
 - firebase
 - toast
 - markdownit

## Tips 3. SSRで発生する問題と解決方法

SSR(初回レンダリング)のときだけエラーになることがよくあります。
これはSSRを考慮していないVueのライブラリが原因であることが多いです。SSRを考慮していないライブラリは、importやVue.use()の際に初期化処理などでwindowオブジェクトやdocumentオブジェクトにアクセスしてしまいます。サーバーサイドには当然ブラウザ由来のオブジェクトであるwindowやdocumentは存在しないのでエラーとなってしまいます。

１． process.browserを使う(FAQに記載のある方法)

```js
if (process.browser) {
  import('hoge')
}
```

２． nuxtのプラグインの場合ssrオプションをfalseにする。

nuxt.config.js

```js
plugins: [
  {src: '~/plugins/my-plugin.js', ssr: false} 
]
```

３． OSSのライブラリならIssueをssrやnuxtで検索する。

利用者の多いOSSの場合はIssueを検索すると問題を解決するのに役立つIssueが見つかることがよくあります。

## Tips 4. babel/eslint を適用させない方法

Nuxtのデフォルトの設定では、node_modules外のJSはすべてbabelとeslintが適用されます。
これはnpm管理外のライブラリなどを単純にコピーしてきて使いたい場合などに困ることになります。

これを解決するには、nuxt.config.jsのbuild.extendプロパティを使ってbabelやeslintの適用除外のディレクトリを作成します。

vendorディレクトリをeslintとbabelの対象外にする例

```js
  build {
    extend (config, ctx) {
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules|vendor)/,
        })
      }
      config.module.rules = config.module.rules.map((rule) => {
        if (rule.loader === 'babel-loader') {
          rule.exclude = /node_modules|vendor/
        }
        return rule
      })
    }
  }
```

## Tips 5. JSのサイズを小さくするために

何も考えずにライブラリをどんどん導入すると初期レンダリングに必要なJSのサイズが肥大化していきます。これは初期レンダリングのパフォーマンスに大きな影響を与えるため、導入するライブラリのファイルサイズには常に気を払っておく必要があります。
特にUIライブラリはファイルサイズが大きく、ほとんどのライブラリは導入するとNuxtが警告を出す300KBを超えてしまいます。

### ファイルサイズの大きいライブラリを見つける

`nuxt build --analyze` コマンドを使うとどのライブラリが大きいのか分かりやすく表示してくれます。

### 複数のページでimportするライブラリはbuild.vendorに指定する

pagesディレクトリの.vueファイルでimportするライブラリには注意が必要です。デフォルトでは各ページごとにライブラリが取り込まれてしまいます。

nuxt.config.js の build.vendor に指定するとページごとではなく共通のJSファイルに取り込まれます。

```js
build: {
  vendor: ['axios']
}
```

### build.extractCSS を設定してCSSをJSから分離する

nuxt.config.jsのbuild.extractCSSプロパティを使うとCSSをJSから分離することができます。
ダウンロードやパースするサイズが減るわけではありませんが、個々のファイルサイズを小さく保つことはブラウザのキャッシュに留まりやすくするなどの効果もあるかと思います。

### 非同期コンポーネントとwebpackのcode splittingを使う

Vueの[非同期コンポーネント](https://jp.vuejs.org/v2/guide/components.html#%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88)と[webpackのcode splitting](https://webpack.js.org/guides/code-splitting/)という機能を使うことで、ページの初回表示にコンポーネントをダウンロードすることができます。

```js
export default {
  components: {
    'el-select': _ => import('element-ui/lib/select')
  }
}
```

## Tips 6. WebStormで楽をする

1. jsdoc形式で型を指定しておくと入力補完できて便利です。

    ```js
    /** @param {ActionContext.<any,any>} context */
      async find(context) {
       ...
    }
    ```
    
2. TypeScript用の型定義(index.d.ts)を置いておくとTypeScriptじゃなくても認識してくれます。Vueや公式のライブラリのリポジトリから定義を持ってきてindex.d.tsにコピペして、jsdocコメントで型指定しておくと補完が効いて嬉しいです。

3. SFCのテンプレートでもscript部に下記のように書いておけば補完できます。
    
    ```js
    /** @property {Hoge} hoge  */
    ```
    
4. @や~を認識させる。
    Nuxtでは@や~がwebpackのエイリアスになっていてソースのルートを表します。
    WebStorm 2017.3 からはwebpack.config.jsをプロジェクトのルートに配置してWebpackの設定で読み込ませることで、これらを認識させて入力補完できます。
    
    webpack.config.jsの例
  
    ```js
    module.exports = {
      resolve: {
        extensions: ['.js', '.json', '.vue', '.ts'],
        root: path.resolve(__dirname),
        alias: {
          '@': path.resolve(__dirname),
          '~': path.resolve(__dirname),
        },
      },
    }
    ```

### Tips 7. エラーハンドリング

Nuxtではサーバー側の処理でエラーが発生する場合とクライアント側の処理でエラーが発生する場合で使用されるテンプレートが異なります。そしてサーバー側のテンプレートはSFCではないため、扱いが面倒です。サーバー側のエラーは可能な限りtry-catchを使って拾うようにして、クライアント側のエラーテンプレートを使ってエラー表示できるようにすると便利です。
サーバー側のエラーの原因になるのは、ほとんどの場合asyncData()/fetch()内の処理です。これらの内容をすべてtry-catchで囲んでしまうのもいいかもしれません。

サーバー側でエラーが発生した場合のエラーページは`app/views/error.html`に記述します。`nuxt.config.js`のtemplatesプロパティを使うと別のパスから読み込むこともできます。

```js
module.exports = {
  templates: [{
    src: '~/views/error.html',
    dst: 'views/error.html'
  }]
}
```

## 黒魔術のご紹介(asyncDataのmixin)

Nuxtで不便に感じたところがあります。下記の２つです。

1. mixin に `asyncData()` や `fetch()` を書いても実行されない
2. `asyncData()` と `methods` でvuexのアクションの呼び出し方が違う

それぞれもう少し詳しく説明します。

### 1. mixin に `asyncData()` や `fetch()` を書いても実行されない

まずこちらのmixinを使ったコードを見てください。

```js
const mixinA = {
  async asyncData() {
    return {a: 'aaa'}
  }
}
export default  {
  mixins: [mixinA],
  async asyncData() {
    return {b: 'bbb'}
  }
}
```

このようにmixin内に`asyncData()`がある場合、{ a: 'aaa', b: 'bbb' } という結果をコンポーネントの`asyncData()`で返したことにして欲しいのですが、現在のNuxt.js(rc11)ではmixinAのasyncData()は呼ばれないため、期待どおりの動作にはなりません。
ちなみに、Vue.jsの`data()`はmixinに対応しており、マージされた結果を使うことができます。
    
### ２．`asyncData()` と `methods` でvuexのアクションの呼び出し方が違う

 - asyncData(context)の場合

このようにasyncData()に渡されたcontextパラメータを介してstoreのdispatchを呼び出します。

```js
asyncData(context) {
  context.store.dispatch('books/findAll')
}
```

 - methodsの場合

このように`mapActions()`で定義してから`this`を経由して呼び出します。

```js
import {mapActions} from 'vuex'
export default {
  methods: {
    ...mapActions({
      findAllBooks: 'books/findAll'
    }),
    hoge () {
      this.findAllBooks()
    }
  }
}
```

同じことをしたいのに、場所によって書き方を分けなければならず、アクションの呼び出しを移動した際に書き直しが必要になるなど、フラストレーションが溜まります。

### 黒魔術(nuxtend)で解決

これらの不満を解決するために、[nuxtend](https://github.com/dotneet/nuxtend) というライブラリを作りました。

nuxtendの機能は以下のとおりです。

- mixinのasyncData()やfetch()も呼ばれる
- data()と同じようにmixinとコンポーネントのasyncData()の結果はマージされる
- asyncData() から this を経由してmethodsの関数を呼べる

それぞれの機能を使ったコード例です。

```js
import nuxtend from 'nuxtend'
import {mapActions} from 'vuex'

const mixinA = {
  async asyncData (context) {
    const books = await this.findBooks()
    return {
      books
    }
  },
  methods: {
    ...mapActions({'findBooks': 'books/find'})
  }
}

export default nuxtend({
  mixins: [mixinA],
  async asyncData (context) {
    const audios = await this.findAudios()
    return {
      audios
    }
  },
  mounted () {
    console.log(this.audios)
    console.log(this.books)
  },
  methods: {
    ...mapActions({
      'findAudios': 'audios/find'
    })
  }
})
```

## nuxt-device-detect のご紹介

先日簡単なNuxtモジュールを公開しましたので紹介します。

[nuxt-device-detect](https://www.npmjs.com/package/nuxt-device-detect)

モバイル/タブレット/デスクトップを判定して処理を分けるのに便利なフラグを`context`とコンポーネントのインスタンスに追加してくれるモジュールです。下記のフラグが追加されます。

 - isDesktop
 - isMobile
 - isTablet
 - isMobileOrTablet

今時のCSSフレームワークの多くにデバイス別に表示を分けるためのクラスが用意されていますが、できれば見えないだけでなくDOMのレンダリングをしたくない人も多いと思います。こちらのモジュールで追加したフラグで`v-if`などで分岐することで、DOMのレンダリングを抑えることができます。

### (使用例1) デバイス別の表示の切り替え

```html
<template>
    <section>
        <div v-if="$device.isDesktop">
            Desktop
        </div>
        <div v-else-if="$device.isTablet">
            Tablet
        </div>
        <div v-else>
            Mobile
        </div>
    </section>
</template>
```

### (使用例2) 動的なレイアウトの切り替え

```js
export default {
    layout: (ctx) => ctx.isMobile ? 'mobile' : 'default'
}
```

## まとめ

 - ドキュメントをまずしっかり読みましょう。FAQも見ましょう。
 - awesome-nuxtやnuxt-community/modules を見ておきましょう
 - nuxt.config.js を研究しておきましょう
 - JSのサイズに気を配りましょう