Vue + Webpack コンポーネント開発(練習環境)初体験

✍🏼 作成日 2016年04月29日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください

はじめに

他の人の vue マルチページフレームワークを研究してみたところ、ほとんどが package.json ファイルを直接コピーしてローカルで npm install を実行するか、公式の vue-cli ツールを使ってプロジェクトを生成する方法でした。確かに理解はできますが、記憶に残らないため、.vue をコンポーネントとして使用するプロジェクトをゼロから構築する練習が必要だと考え、このプロジェクトを作成しました。

.vue コンポーネントを使用する場合、以前のように jQuery のように vue.js をページに直接読み込むのではなく、webpack + babel + 各種 loader ツールを使用して .vue ファイルをコンパイルし、html をバンドル生成する必要があります。

FBI警告

注意 : これは最も基本的な初体験であるため、正式な開発で必須となる loaderplugin はインストールしていません。公式チュートリアルに従って手動で入力することで、特に上級チュートリアルで扱う親子コンポーネント間のパラメータ受け渡し、スコープ付きスロット、再帰コンポーネント、slot などの複雑な部分を深く理解するためです。したがって、この設定は正式な開発の参考設定として使用することはできず、vue コンポーネントの動作原理を理解するための練習プロジェクトとしてのみ利用してください。

設定説明

以下の設定の詳細説明は後述しますが、読みたくない場合は以下の package.json を直接コピーしてください。ただし、記憶に定着させるためには手動で入力することをお勧めします。

さっそく始めましょう。

まず、webpack+vue を使用するため、必要なパッケージをインストールします。ここでは vue@2.2.4webpack@1.12.2 を使用します:

1
npm install webpack@1.12.2 vue@2.2.4 --save-dev

次に babel と対応する loader です。ここでは es2015 の設定を使用し、最新のものを利用します:

1
npm install babel bebel-core babel-loader babel-preset-es2015 --save-dev

そして webpack の必須 loader です。css-loadercss 内の url() リソースを処理し、style-loaderrequire された css を抽出して style タグに挿入し、ページの head 部分に追加します。html-webpack-plugin はエントリーファイルの jshtml に変換し、エントリーファイル内の各種リソースは各種 loader によって処理された後、生成された html に挿入されます。extract-text-webpack-pluginjs から style タグを介して head に追加されたスタイルを個別の .css ファイルに抽出します:

1
npm install css-loader style-loader html-webpack-plugin extract-text-webpack-plugin@1.0.1 --save-dev

最後に vue 関連のものです。.vue ファイルには少なくとも template/style/script の3つのタグが含まれるため、3つの loader と総合的な vue-loader の計4つの loader が必要です:

vue-html-loaderwebpack の公式 html-loader のフォークで、作者は webpack.config.jsmodule.export.vue オブジェクト上で html オプションを使用して vue 関連の html を個別に設定できるようにするためにここに配置しています(このプロジェクトでは vue-loader をインストールすれば十分ですが、説明のために一緒にインストールしています)。

vue-style-loader.vueファイル内のstyleセクションの内容を処理するために使用され、webpackの公式style-loaderforkです(このプロジェクトではvue-loaderをインストールすれば十分ですが、ここでは説明のために記載しています)。

vue-template-compiler.vueファイル内のtemplateセクションの内容を処理するために使用されます。コンパイル後のファイルで他の処理を行う場合(つまりbuild toolsを記述する場合)を除き、これは必須ではありません。なぜならvue-loaderは既にデフォルトでこれを使用しているからです(このプロジェクトではvue-loaderをインストールすれば十分ですが、ここでは説明のために記載しています)。

vue-loader.vue拡張子の内容を処理するために使用され、関連する内容が検出されると、上記の3つのloaderを呼び出して処理を行います。

1
npm install vue-html-loader vue-loader vue-style-loader vue-template-compiler --save-dev

最後に開発用のwebpack-dev-serverです。ここでは1.12.1バージョンをインストールします:

1
npm install webpack-dev-server --save-dev

以下は全体のpackage.json設定ファイルです。各package.jsonフィールドの具体的な意味については、このサイトを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "vue-components",
"version": "0.0.1",
"description": "vue components test",
"main": "app/app.js",
"scripts": {
"dev": "webpack-dev-server --hot",
"build": "webpack"
},
"keywords": [
"vue",
"components"
],
"author": "xheldon",
"license": "MIT",
"dependencies": {
"vue": "^2.2.4"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.24.0",
"babel-loader": "^6.4.1",
"babel-preset-es2015": "^6.24.0",
"css-loader": "^0.27.3",
"extract-text-webpack-plugin": "^1.0.1",
"html-webpack-plugin": "^2.28.0",
"style-loader": "^0.16.0",
"vue-html-loader": "^1.2.4",
"vue-loader": "^11.3.1",
"vue-style-loader": "^2.0.4",
"vue-template-compiler": "^2.2.4",
"webpack": "^1.12.2",
"webpack-dev-server": "^1.12.1"
}
}

プロジェクト説明

依存関係のインストールが完了したので、次にwebpackの設定を見ていきます。vue公式ドキュメントのコンポーネント部分を早くテストしたいため、すべてを簡素化しています:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
var path = require('path');
var webpack = require('webpack');
var HtmlwebpackPlugin = require('html-webpack-plugin');

// 常用配置,项目较小不抽出了
var ROOT_PATH = path.resolve(__dirname); //根路径
var APP_PATH = path.resolve(ROOT_PATH, 'app'); //开发路径
var BUILD_PATH = path.resolve(ROOT_PATH, 'build'); //输出路径
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
entry: {
app: path.resolve(APP_PATH, 'app.js'),
},
output: {
path: BUILD_PATH,
filename: 'bundle.js', //因为只有一个入口文件, 因此直接写死了
},
resolve: {
alias: {
//注意, 这里导入的是/node_module/vue/dist/vue.js, 跟 vue-router.js 的不同
vue: 'vue/dist/vue.js',
},
},
//开启 dev source map
devtool: 'eval-source-map',
//开启 dev server
devServer: {
historyApiFallback: true,
hot: true, //HMR
inline: true,
progress: true,
},
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue',
},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('css-loader'),
},
{
test: /\.js$/,
loader: 'babel',
include: ROOT_PATH,
exclude: /node_modules/,
},
{
test: /\.html$/,
loader: 'vue-html',
},
],
},
vue: {
loaders: {
css: ExtractTextPlugin.extract('css-loader'),
},
},
plugins: [
new HtmlwebpackPlugin({
title: 'Vue component test',
filename: 'this_is_final_filename_address_you_visit_in_browser.html', //生成的最终文件名
template: 'app/this_is_main_page_which_you_put_components_into.html', //放置组件的地方, 一般是一个 body 下一个孤零零的 app 标签即可.
inject: true,
}),
],
};

この設定について、いくつか説明が必要です。

上から順に、まずaliasです。vue-routerを使用したことがある人はこの設定が必要ないかもしれませんが、.vueコンポーネントを使用するプロジェクトではこの設定が必須です。なぜなら、使用するvuejsタイプを指定する必要があるからです。このプロジェクトのnode_module/vue/dist/フォルダ内のファイルを見ると、vue.jsvue.common.jsの2種類があります。vuetemplateコンポーネントをコンパイルする際にはcompiler.jsが必要で、その目的はtemplate内のhtml内容をrender関数にコンパイルすることです:

コンパイル前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div id="app">Hello {{who}}</div>
<script>
new Vue({
el: '#app',
data: { who: 'Vue' },
});
</script>
``` コンパイル後: ```js
<div id="app"></div>
<script>
new Vue({
el: '#app',
render: function () {
with (this) {
__h__(
'div',
{ staticAttrs: { id: 'app' } },
['\n Hello ' + __toString__(who) + '\n'],
''
);
}
},
data: { who: 'Vue' },
});
</script>

そしてvuecompilertemplateをコンパイルした後のjsを実行する際、render関数が存在する場合は直接renderを実行し、templateフィールドの内容は無視されます。コンパイル後のrenderを実行するタスクは、vue.common.jsによって行われます。したがって:

1
vue.js = vue.common.js + compiler.js;

そのため、vue-routerを使用している場合、そのpackage.jsonmainフィールドはnode_module/dist/vue.common.jsを指しています。これをそのままプロジェクトにコピーすると、実行時にvue.common.jsruntimeエラーといったメッセージが表示される可能性があります。

次に説明する必要があるのは、この css-loaderstyle-loader、そして vue-style-loader についてです。style-loader があるのに、なぜ vue-style-loader が必要なのでしょうか? vue-style-loader の説明を確認したところ、これは単に style-loaderfork であり、.vue ファイルを個別に処理するため、またユーザーが vue の設定をより明確に行えるように、webpack@1.x の設定ファイルの vue フィールドに追加されていることがわかりました。extract-text-webpack-plugin プラグインの設定を:

1
2
3
4
5
vue: {
loaders: {
css: ExtractTextPlugin.extract('vue-style-loader', 'css-loader');
}
}

から:

1
2
3
4
5
vue: {
loaders: {
css: ExtractTextPlugin.extract('style-loader', 'css-loader');
}
}

に変更することで、コンパイル後の結果が同じであることを確認しました。

デフォルトでは、vue-loader は自動的に vue-style-loader を使用するため、.vue ファイル内で @import を使用して css を読み込まない場合、手動で vue-loader-stylevue.loaders フィールドに追加する必要はありません。vue-loader は自動的に .vue ファイル内の style タグの内容を処理し、style タグとしてページに挿入します。しかし、.vue ファイル内の style タグで @import を使用して css ファイルを読み込む必要がある場合は、module.exports.vue で個別に設定する必要があります。vue フィールドから vue-style-loader を削除してテストすることができます:

1
2
3
4
5
6
7
8
9
10
11
12
13
module: {
loaders:[
{
test: /\.vue$/,
loader: 'vue'
}
]
},
vue:{
// loaders: {
// css: 'vue-style-loader!css-loader'
// }
}

さらに、エントリーファイルの js 内の require('xxx.css') は、デフォルトの module.exports.module.loader によって処理されます。これは、デフォルトの loaderextract プラグインを使用し、module.exports.vue では extract プラグインを使用しないことで確認できます。結果から、エントリーファイル内の css は抽出されますが、.vue ファイル内の @import で読み込まれた css は抽出されないことがわかります。

もしエントリーファイルで require された css ファイルを個別に抽出する必要がある場合は、module.exports.module.loaderextract-text-webpack-plugin を設定する必要があります。

注意: vue-style-loadermodule.exports.vue.loaders フィールドに配置するのは、.vue ファイル内の style タグの内容を個別の .css ファイルとして抽出し、ページに link として挿入するためです。style-loadercss-loader をデフォルトの module.exports.module.loaders に配置しても、vue 内の style タグの内容を処理することはできません。少なくとも Vue 1.x バージョンと webpack 1.x バージョンでは無効です。webpack 2.x バージョンでは、サードパーティのフィールドが削除され、module.export 内に自由にフィールドを追加することが制限されています。

最後に、css-loaderstyle-loaderは常にセットで記述されます。なぜなら、css-loaderの役割は@importurl()属性値内の依存関係を解決することであり、単独では実質的に意味を成さないからです。style-loaderこそがcssを処理し、jsにバンドルして、最終的にhead内(挿入位置は設定可能)に<style>タグとして挿入するloaderなのです。

最後にextract-text-webpack-pluginについて説明します。このプラグインは3つの引数を受け取ります:

第1引数はオプションで、loaderを渡します。cssスタイルが抽出されなかった場合にこのloaderが使用されます。
第2引数はcssファイルをコンパイル・解析するためのloaderで、前述の例のcss-loaderのように必須です。
第3引数は追加オプションで、現在はpublicPath(現在のloaderのパスを指定)のみが利用可能です。

第1引数が必要なケースは、スタイルが抽出されない状況を理解すれば明らかになります。
code splittingをご存知の方ならわかるように、ページロード時に使用されないコードがある場合、code splittingによってそのコードを分割し、別ファイルとして按需ロードを実現できます。

この分割されたコード内でrequireによりスタイルファイルをインポートしている場合、ExtractTextPluginを使用してもこれらのスタイルコードは抽出されません。
抽出されないコードに対してloaderで処理を行うのが、ExtractTextPlugin.extractの第1引数の役割です。

さて、設定ファイルの話はこれで終わりです。次に、このプロジェクトの動作原理について説明しましょう。

百聞は一見に如かず。まずはコード画面をご覧ください:

项目结构

(画像が小さい場合は右クリックで新しいタブで開いてください)

矢印が示す通り、1ページには少なくとも1つのコンポーネントが必要です。ここでは1ページ1コンポーネントとして記述し、他のコンポーネントをimportしていません。

したがって、1ページには最低3つのファイル(.vueファイル、.jsエントリーファイル、.htmlコンポーネント挿入ファイル)が必要です。

htmlにはappというコンポーネント名を記述し、エントリーファイルでvueをインスタンス化してappコンポーネントを使用します。このappコンポーネントのテンプレートはindex.vueから取得し、対応するcssjs、そしてmvvmの特徴であるデータバインディングもindex.vue内に記述します。

「エントリーファイルのjsはどうやって同ディレクトリのhtmlファイルを見つけるのか?」と疑問に思う方もいるでしょう。実はこれはwebpack.config.js設定ファイルであらかじめ定義されています:

1
2
3
4
5
6
7
8
plugins: [
new HtmlwebpackPlugin({
title: 'Vue component test',
filename: 'this_is_final_filename_address_you_visit_in_browser.html', //生成的最终文件名
template: 'app/this_is_main_page_which_you_put_components_into.html', //放置组件的地方, 一般是一个 body 下一个孤零零的 app 标签即可.
inject: true,
}),
];

このhtml生成プラグインがjsエントリーファイルに、必要なテンプレートがappディレクトリのxxx.htmlから来ること、そして最終的にバンドルされたbundle.jsがこの中にinjectされ、最終ページが生成されることを伝えています。

また、疑問に思う学生もいるかもしれません。エントリーポイントの js ファイルで、vue のインスタンス化時に使用される components.App は、コンパイルプロセス中に this_is_main_page_which_you_put_components_into.html ファイル内の <app> コンポーネント参照を見つけるのか、それとも runtime 時に、最終的にバンドルされた bundle.js から実行され、this_is_final_filename_address_you_visit_in_browser.html ページ内の <app> タグを探すのか?答えは後者です。なぜなら、先ほど述べた HtmlwebpackPlugin プラグインは html の生成とバンドル後の bundle.js の注入のみを担当しており、vuebundle.js にバンドルされた後、vue のインスタンス化時に初めて <app> タグを探すからです。

こう見ると、直接 script タグで vue.js ファイルを参照してレンダリングする効果と同じですが、webpack のような開発方法はコンポーネントを分離し、開発プロセス中のコード/コンポーネント構造をより明確にします。また、直接 vue.js を参照するのはフロントエンドの runtime render であり、一方で compiler render 後に直接実行される後者の方が効率が高いです。

次に、効果ページです:

页面效果图
(画像が小さい場合は右クリックで新しいタブで開いてご覧ください)

画像を見れば意味がわかりますね。

まだ不明点がある場合はドキュメントを参照してください。

今回はここまでです。vuewebpack に関する 注意点/詳細/設計思想 についてはまだ話すことがたくさんありますので、また後ほど~

- EOF -
この記事の初出: Vue + Webpack コンポーネント開発(練習環境)初体験 - Xheldon Blog