WebpackでVue.jsをバンドルしたら画面に表示されない問題の解決(完全ビルドとランタイム限定ビルド関連)
あらすじ
「ちょっとしたSPAを作るのにNuxt.jsを使うのは重いかな🤔」と思った砂糖は、WebpackでVue.jsをバンドルできるように実装を始めるのだった。
環境
webpack 5.11.0
vue 2.6.12
事象
WebpackでVue.jsをバンドルしたファイルを読み込んだindex.html
をブラウザから開いたところ、画面に何も表示されていなかった。Chromeの開発者ツールのConsoleには特にエラー出力されていない。
同じく開発者ツールのElementsを見てみると、<!--function(e,n,r,o){return Fe(t,e,n,r,o,!0)}-->
という謎のコメント文[1]が表示されていた。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpack-vue</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<!--function(e,n,r,o){return Fe(t,e,n,r,o,!0)}-->
<script src="bundle.js"></script>
</body>
</html>
作成したファイル(参考用)
package.json
{
"scripts": {
"build": "webpack"
},
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"vue": "^2.6.12",
"webpack": "^5.11.0",
"webpack-cli": "^4.2.0"
}
}
webpack.config.js
HTML Webpack Pluginでdist/index.html
を生成している。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new HtmlWebpackPlugin()]
}
src/index.js
ネタバレ: 大体こいつが犯人
import Vue from 'vue'
new Vue({
el: '#app',
data: () => ({
counter: 0
}),
template: '<div>Counter: {{ counter }}</div>'
})
src/index.ejs
HTML Webpack Pluginで生成されるindex.html
のテンプレート。出力したJavaScriptファイルを読み込むscript
タグが自動で挿入される。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>webpack-vue</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
調査
minify化をやめてみる
出力されたコードを追ってみようにもminify化されていると読みにくいので、minify化しないようにwebpack.config.js
の設定を書き換える(optimization.minimize
を追加しfalse
を指定した)。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new HtmlWebpackPlugin()],
optimization: {
minimize: false
}
}
バンドル後にブラウザでdist/index.html
を開き、開発者ツールで確認してみる。minify化されていたときは<!--function(e,n,r,o){return Fe(t,e,n,r,o,!0)}-->
だったところが、 <!--function (a, b, c, d) { return createElement(vm, a, b, c, d, true); }-->
になっていた。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpack-vue</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<!--function (a, b, c, d) { return createElement(vm, a, b, c, d, true); }-->
<script src="bundle.js"></script>
</body>
</html>
Vue.jsのビルドの種類
いくらか意味が分かりやすくなったため、<!--function (a, b, c, d) { return createElement(vm, a, b, c, d, true); }-->
で検索をしてみると次のページが見つかった。
ここで紹介されている方法では、import Vue from 'vue'
と指定したときにvue/dist/vue.esm.js
を読み込むようにエイリアスを設定している(ちなみにvue
のpackage.json
を見る限りエイリアスを設定しない場合はvue/dist/vue.runtime.esm.js
を読み込んでいる模様)。
vue.esm.js
とvue.runtime.esm.js
の違いは「(テンプレート文字列をコンパイルするための)コンパイラのコードが含まれるかどうか」であり、問題が起きたときに読み込んでいたvue.runtime.esm.js
にはコンパイラのコードが含まれない(=コンパイルが行われない)。つまり、 「テンプレートをコンパイルしたらちゃんと表示される」 という意味になる。
webpack.config.js
にエイリアスを追加してバンドルしたところ、ひとまずブラウザで表示が確認できるようになった。
エイリアスを追加したwebpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new HtmlWebpackPlugin()],
optimization: {
minimize: false
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
}
}
vue$
(末尾に$
)はvue
の完全一致を示しており、$
を付けない場合にvue
から始まるライブラリをインポートしていた場合は大変なことになるかもしれない🤔
バンドルの軽量化
ランタイム限定ビルドと完全ビルド
公式ドキュメントでは、コンパイラを含まないvue.runtime.esm.js
をランタイム限定ビルド、コンパイラを含むvue.esm.js
を完全ビルドと呼んでいる[2]。
完全ビルドは次のコードのようなtemplate
や、DOMのHTMLをレンダリング関数にコンパイルできる。
import Vue from 'vue'
new Vue({
el: '#app',
data: () => ({
counter: 0
}),
template: '<div>Counter: {{ counter }}</div>'
})
ランタイム限定ビルドはコンパイル用のコードが除かれているため、レンダリング関数を使用する。
import Vue from 'vue'
new Vue({
el: '#app',
data: () => ({
counter: 0
}),
render(h) {
return h('div', `Counter: ${this.counter}`)
}
})
完全ビルドはコンパイラのコードの分だけランタイム限定ビルドよりも大きくなるため、できればランタイム限定ビルドを使用したい。
vue-loaderの使用
vue-loader
を使用するとテンプレートが事前にコンパイルされるそうなので、vue-loader
を用いて単一ファイルコンポーネントを導入してみることにする。
手順
1. パッケージの追加
単一ファイルコンポーネントに必要なvue-loader
とvue-template-compiler
を追加する。
yarn add --dev vue-loader vue-template-compiler
2. バンドル設定の編集
webpack.config.js
からresolve.alias
を削除し、module
にvue-loader
を、plugins
にVueLoaderPlugin
を追加する。
vue-loaderとVueLoaderPluginを追加したwebpack.config.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new VueLoaderPlugin(), new HtmlWebpackPlugin()],
optimization: {
minimize: false
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
}
module.rules
には単一ファイルコンポーネントのテンプレートに必要な部分しか設定していないため、CSS等を使用する場合は適宜追加する。
3. 単一ファイルコンポーネントの作成
src/App.vue
にコンポーネントを実装する。
<template>
<div>Counter: {{ counter }}</div>
</template>
<script>
export default {
data: () => ({
counter: 0
})
}
</script>
4. エントリーファイルの編集
コンパイラを使用しないため、src/index.js
のtemplate
をrender
の形に書き換える。
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
components: {
App
},
render(h) {
return h('app')
}
})
あとがき
普段Nuxt.jsを使っていてVue.jsのビルドを気にしたことがなかったのでハマった🙄 答えは公式ドキュメントに書いてあるものの、問題が起こってからドキュメントを一通り読んだとしても気付けなかったのではと感じている😵
Discussion