Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

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

tabian
画像認識技術を用いて、新しいポスター体験を提供する「ポ写」を開発している全員副業のスタートアップです! エンジニアが自由に、でも真面目に技術に向き合える環境、それは最新技術にとことんこだわれることだと思って日々Qiitaの更新頑張ってます。
https://tabian.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away