LoginSignup
16
15

More than 3 years have passed since last update.

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

Posted at

Nuxt.js のコンポーネント

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

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

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

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

2 directories, 4 files

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 ファイルを作って、
最初に全てのコンポーネントを登録しておく方法です。

plugins/components.js
import Vue from 'vue'

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

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

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

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 を使って省力化する方法もあります。

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

VuetifyLoaderPlugintreeShake する際に import する

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

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

export default {
  components: {
    AppWelcomeMessageBoard
  }
}

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

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

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

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

この vuetify の設定の所を、

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.jsmodules に書きましょう。

@nuxtjs/vuetify を使わない場合

この辺 に書かれていることを、自分で書きます。

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 で公開しておきました。

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

16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15