First Experience with Vue + Webpack Component-Based Development (Practice Environment)

✍🏼 Written on Apr 29, 2016   
❗️ Note: it has been days since this article was written, please be aware of its timeliness

Preface

After studying others’ vue multi-page frameworks, I found they simply copy the package.json file and run npm install locally, or generate a project using the official vue-cli tool. While this approach is understandable, it’s hard to retain. Therefore, it’s necessary to practice building a project from scratch that uses .vue as components, which led to the creation of this project.

Since .vue components are used, we can’t just include vue.js in the page as we did with jQuery. Instead, we need to compile .vue files using the accompanying tools like webpack + babel + various loaders, and then bundle them into html.

FBI Warning

Important Note: Since this is a basic introductory experience, some essential loaders and plugins for formal development are omitted. The goal here is to manually follow the official tutorials to reinforce understanding, especially advanced topics like parent-child component communication, scoped slots, recursive components, and slot. Thus, this configuration should not be used as a reference for formal development—it’s purely a hands-on project to understand how vue components work.

Configuration Instructions

Detailed explanations for the following configurations can be found later. If you don’t want to read them, you can directly copy the package.json below. However, for better retention, it’s recommended to type it out manually.

Without further ado, let’s begin.

First, since this is webpack + vue, the corresponding packages are essential. Here, we use vue@2.2.4 and webpack@1.12.2:

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

Next are babel and its corresponding loaders. We use the es2015 preset—just go with the latest:

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

Then come the must-have loaders for webpack:

  • css-loader handles resources in url() within css.
  • style-loader extracts css required via require and injects it into style tags in the page’s head section.
  • html-webpack-plugin converts entry js files into html, with all resources processed by various loaders and inserted into the generated html.
  • extract-text-webpack-plugin extracts styles appended to head via style tags in js into separate .css files:
1
npm install css-loader style-loader html-webpack-plugin extract-text-webpack-plugin@1.0.1 --save-dev

Next are vue-related tools. Since a .vue file contains at least three tags—template, style, and script—three loaders are needed to process them, plus a master vue-loader, totaling four loaders. Here:

vue-html-loader is a fork of webpack’s official html-loader, included here just to allow configuring vue-related html separately in the module.export.vue object in webpack.config.js (this project only requires vue-loader; this is just for explanatory purposes).

vue-style-loader is used to process the content within the style tags of .vue files. It is a fork of webpack’s official style-loader (installing vue-loader in this project is sufficient; this is just an additional explanation).

vue-template-compiler is used to process the content within the template tags of .vue files. It is only necessary to configure it separately if you intend to use its compiled files for other purposes (e.g., writing build tools). Otherwise, it is not required, as vue-loader already uses it by default (installing vue-loader in this project is sufficient; this is just an additional explanation).

vue-loader is used to handle .vue files. When encountering such content, it invokes the three aforementioned loaders for processing.

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
npm install vue-html-loader vue-loader vue-style-loader vue-template-compiler --save-dev
```

Finally, there’s the development tool `webpack-dev-server`, for which we install version `1.12.1`:

```js
npm install webpack-dev-server --save-dev
```

Below is the complete `package.json` configuration file. For detailed explanations of each `package.json` field, refer to [this website](http://json.is/).

```js
{
"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"
}
}
```

## Project Overview

Okay, the dependencies are installed. Next, let’s look at the `webpack` configuration. Since the goal is to quickly test the component section of the `vue` official documentation, everything is kept minimal:

```js
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,
}),
],
};
```

A few things need to be clarified about this configuration.

From top to bottom, first is the `alias`. Those who have used [`vue-router`](https://github.com/vuejs/vue-router/) might not need this configuration, but projects using `.vue` components must include it because it specifies the type of `vue` `js` to be used. Check the files in the `node_module/vue/dist/` folder of this project—there are two types: `vue.js` and `vue.common.js`. When `vue` compiles `template` components, it requires a `compiler.js` to convert the `html` content in the `template` into a `render` function:

Before compilation:

````html
<div id="app">Hello {{who}}</div>
<script>
new Vue({
el: '#app',
data: { who: 'Vue' },
});
</script>
``` After compilation: ```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>
```

When `vue` detects a `render` function in the compiled `js` during runtime, it directly executes the `render` function, and the content under the `template` field is ignored. The task of executing the compiled `render` function is handled by `vue.common.js`. Therefore:

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

So, if you’re using `vue-router`, its `package.json`'s `main` field points to `node_module/dist/vue.common.js`. If you directly copy this into your project, you may encounter runtime errors related to `vue.common.js`.

Next, let's discuss `css-loader`, `style-loader`, and `vue-style-loader`. Why do we need `vue-style-loader` when we already have `style-loader`? After reviewing the documentation of `vue-style-loader`, it becomes clear that it is merely a fork of `style-loader`, but it was created specifically to handle `.vue` files. Additionally, to make Vue configuration clearer for users, it was added to the `vue` field in `webpack@1.x` configuration files. This can be confirmed by changing the configuration of the `extract-text-webpack-plugin` from:

```js
vue: {
loaders: {
css: ExtractTextPlugin.extract('vue-style-loader', 'css-loader');
}
}

to:

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

The compiled results remain identical, which verifies this.

By default, vue-loader automatically uses vue-style-loader. Therefore, if you don’t @import any css in .vue files, you don’t need to manually include vue-loader-style in the vue.loaders field. vue-loader automatically processes the content within the style tags of .vue files and injects it into the page via style tags. However, if you need to @import css files within the style tags of .vue files, you must configure it separately in module.exports.vue. This can be tested by removing vue-style-loader from the vue field:

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'
// }
}

Furthermore, require('xxx.css') in the entry js file is handled by the default module.exports.module.loader. This can be confirmed by using the extract plugin in the default loader while omitting it in module.exports.vue. The results show that the css in the entry file is extracted, but the css imported via @import in .vue files is not.

If you need to extract css files required in the entry file separately, you must configure extract-text-webpack-plugin in module.exports.module.loader.

Note: Placing vue-style-loader in the module.exports.vue.loaders field allows the content of style tags in .vue files to be extracted into a separate .css file and linked in the page. However, placing style-loader and css-loader in the default module.exports.module.loaders has no effect on processing style tag content in vue—at least not in Vue 1.x and webpack 1.x versions. webpack 2.x removed support for third-party fields, restricting arbitrary field additions in module.export.

Finally, css-loader and style-loader are always used together because the role of css-loader is to resolve dependencies in @import and url() property values within CSS files. Using it alone is practically useless. style-loader is the one that processes CSS and bundles it into JS, ultimately inserting it into the head (insertion position is configurable) as a <style> tag.

Now, let’s talk about extract-text-webpack-plugin, which accepts three parameters:

The first parameter is optional and takes a loader that can be used when CSS styles are not extracted.
The second parameter is the loader for compiling and parsing CSS files, which is obviously required, as seen in the example above with css-loader.
The third parameter includes some additional options, currently only publicPath, which specifies the path for the current loader.

So when is the first parameter needed? That depends on understanding when styles won’t be extracted. Those familiar with code splitting will know that some code isn’t used when the page loads. Using code splitting, such unused code can be separated into standalone files for on-demand loading.

If these separated files include styles loaded via require, ExtractTextPlugin won’t extract them. For these non-extracted styles, you can use the loader to handle them—this is the purpose of the first parameter in ExtractTextPlugin.extract.

OK, now that we’ve covered the configuration file, let’s discuss how this project works.

A picture is worth a thousand words. Here’s the code interface:

项目结构

(If the image appears too small, right-click to open it in a new tab.)

As the arrows show, each page has at least one component. For simplicity, I’ve written one component per page without importing others.

Thus, each page consists of at least three files: a .vue file, a .js entry file, and an .html file for component insertion.

The html file defines a component named app. The entry file instantiates a vue instance and uses the app component, whose template comes from index.vue. The component’s CSS, JS, and data binding (a hallmark of MVVM) are also written in index.vue.

Some might wonder how the entry js file finds the html file in the same directory. This is actually configured in 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,
}),
];

This HTML generation plugin tells the JS entry file that the required template comes from xxx.html under app, and the final bundled bundle.js is injected into it to generate the complete page.

Some students may ask: In the entry js file, when instantiating vue, does components.App locate the <app> component reference in the this_is_main_page_which_you_put_components_into.html file during the compilation process, or does it run from the final bundled bundle.js at runtime and then search for the <app> tag in the this_is_final_filename_address_you_visit_in_browser.html page? The answer is the latter, because the aforementioned HtmlwebpackPlugin plugin is only responsible for generating the html and injecting the bundled bundle.js. After vue is bundled into bundle.js, it will only look for the <app> tag when instantiating vue.

From this perspective, the effect is the same as directly referencing the vue.js file in a script tag and then rendering. However, the webpack development approach helps us separate components, making the code/component structure clearer during development. Moreover, directly referencing vue.js is frontend runtime render, whereas the other approach executes after compiler render, making the latter more efficient.

Next is the demo page:

页面效果图
(If the image appears too small, right-click to open it in a new tab for a better view.)

The image makes it clear what’s going on.

For further clarification, please refer to the documentation.

That’s all for now. There’s still much to discuss about vue and webpackprecautions/details/design philosophy. Talk later~

- EOF -
Originally published at: First Experience with Vue + Webpack Component-Based Development (Practice Environment) - Xheldon Blog