Posted at

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


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 のバージョンが上がっていくと、

いろいろ構成が変わっていく可能性があります。