Nuxt.jsでcomponentsフォルダのVueコンポーネントを自動でimportする

# Nuxt.js のコンポーネント

Nuxt.js は、ソースファイルのフォルダ構造を決めてくれるので、整理が楽でいいですよね。

Vue コンポーネントは、 `components` フォルダに置くルールになっていますが、
自動で読み込んでくれるわけではありません。

例えば、 `components` フォルダがこんな構造であった場合：

```shell
$ tree components/
components/
├── Logo.vue
├── README.md
├── VuetifyLogo.vue
└── app
    └── welcome
        └── MessageBoard.vue

2 directories, 4 files
```

`vue` コンポーネントにはこんな感じで書くと思います。

```vue
<template>
  <app-welcome-message-board>
    <div>今日はいい天気ですね！</div>
  </app-welcome-message-board>
</template>

<script>
import AppWelcomeMessageBoard from '@/components/app/welcome/MessageBoard'

export default {
  components: {
    AppWelcomeMessageBoard
  }
}
</script>
```

つらたん。。。

# コンポーネントを自動で `import` してほしい！

`import` 文と `components` フィールドに定義するのは冗長で面倒です。

上記は1つのコンポーネントの例ですが、
コンポーネント数が多くなると管理がかなり煩雑になります。

解決アプローチは、2つあります。

## `Vue.component` でグローバルに登録する

`plugins` フォルダに JavaScript ファイルを作って、
最初に全てのコンポーネントを登録しておく方法です。

```javascript:plugins/components.js
import Vue from 'vue'

import AppWelcomeMessageBoard from '@/components/app/welcome/MessageBoard'

Vue.component('app-welcome-message-board', AppWelcomeMessageBoard)
```

1行ずつコンポーネントを登録していくこともできますが、
数が増えてきたらちょっと省力化するのもアリです。

```javascript:plugins/components.js
import Vue from 'vue'

import AppWelcomeMessageBoard from '@/components/app/welcome/MessageBoard'

const components = {
  AppWelcomeMessageBoard
}

Object.entries(components).forEach(([name, component]) => {
  Vue.component(name, component)
})
```

Webpack 配下なので、 `require.context` を使って省力化する方法もあります。

```javascript:plugins/components.js
import path from 'path'
import Vue from 'vue'

const components = require.context(
  '~/components',
  true,
  /\.(vue|js)$/
)

function camelCase (...args) {
  return args.map(part => part.slice(0, 1).toUpperCase() + part.slice(1)).join('')
}

components.keys().forEach((fileName) => {
  const extName = path.extname(fileName)
  const dirName = path.dirname(fileName).split('/').filter((part, index) => part !== '.')
  const baseName = path.basename(fileName, extName)

  const componentName = camelCase(...dirName, baseName)
  const componentObj = components(fileName)

  Vue.component(componentName, componentObj.default || componentObj)
})
```

### デメリット

しかし、この方法には欠点があります。

* `components` フォルダに置いてあるものの、実際にはどこからも参照されていないコンポーネントが、
  一緒にビルドされてしまう
* 全てのページで、全てのコンポーネントをメモリに展開してしまう

## `VuetifyLoaderPlugin` で `treeShake` する際に `import` する

Vuetify.js をフレームワークとして使用している場合は、

```javascript
import AppWelcomeMessageBoard from '@/components/app/welcome/MessageBoard'

export default {
  components: {
    AppWelcomeMessageBoard
  }
}
```

の部分を動的に挿入してビルドしてくれる、 `vuetify-loader/lib/plugin` が利用できます。

Nuxt.js が `>= 2.9.0` である場合は、 `nuxt.config.js` の `buildModules` を使って、

```javascript:nuxt.config.js
export default {
  ...
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
    '@nuxtjs/vuetify'
  ],
  /*
  ** vuetify module configuration
  ** https://github.com/nuxt-community/vuetify-module
  */
  vuetify: {
    ...
  },
  ...
}
```

となっていると思います。

この `vuetify` の設定の所を、

```javascript:nuxt.config.js
import path from 'path'
import glob from 'glob'

const COMPONENTS_DIR = 'components'

export default {
  ...
  /*
  ** vuetify module configuration
  ** https://github.com/nuxt-community/vuetify-module
  */
  vuetify: {
    ...
    treeShake: {
      loaderOptions: {
        /**
         * This function will be called for every tag used in each vue component
         * It should return an array, the first element will be inserted into the
         * components array, the second should be a corresponding import
         *
         * originalTag - the tag as it was originally used in the template
         * kebabTag    - the tag normalised to kebab-case
         * camelTag    - the tag normalised to PascalCase
         * path        - a relative path to the current .vue file
         * component   - a parsed representation of the current component
         */
        match (originalTag, { kebabTag, camelTag }) {
          const parts = kebabTag.split('-')

          const REQUIRED_MIN_FILENAME = 3

          if (parts[0].length >= REQUIRED_MIN_FILENAME) {
            for (let i = 0; i < parts.length; i++) {
              const pathPart = parts.slice(0, i)
              const filePart = parts.slice(i)

              const relPath = path.join(COMPONENTS_DIR, ...pathPart, camelTag.substr(-filePart.join('').length))

              const globResult = glob.sync(`${relPath}.{js,vue}`, {
                cwd: __dirname
              })

              if (globResult.length > 0) {
                return [
                  camelTag,
                  `import ${camelTag} from '~/${relPath}'`
                ]
              }
            }
          }
        }
      }
    }
  },
```

とすると、ローカルにファイルがあるかどうかを判定して、
必要な `import` 文を挿入してくれます。

### デメリット

この方法の良くないところは、ビルド時にファイルシステムにアクセスするので、
時間がかかるところです。

キャッシュすると早くなると思いますが、それはまたの機会に。

### Nuxt.js `< 2.9.0` の場合

`nuxt.config.js` の `modules` に書きましょう。

### `@nuxtjs/vuetify` を使わない場合

[この辺](https://github.com/nuxt-community/vuetify-module/blob/master/src/build.ts#L18) に書かれていることを、自分で書きます。

```javascript:nuxt.config.js
const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')

export default {
  ...
  /*
  ** Build configuration
  */
  build: {
    transpile: [
      'vuetify/lib'
    ],
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
      config.plugins.push(new VuetifyLoaderPlugin({
        match (originalTag, { kebabTag, camelTag }) {
          ...
        }
      })
    }
  }
}
```

# ソースコード公開中

サンプルの Nuxt.js プロジェクトを GitHub で公開しておきました。

https://github.com/kulikala/nuxt-vuetify-loader-sample

なお、 Nuxt.js のバージョンが上がっていくと、
いろいろ構成が変わっていく可能性があります。