vue-cli是vue官方出品的一个脚手架工具,通过vue-cli可以快速的搭建一个vue的SPA应用的开发框架。其内部提供了多种vue-js-template,作用就是通过相应的template去配置生成项目初期的内容。其中webpack模板提供了全面丰富的webpack配置,包括热重载、单元测试、静态检查以及css提取等功能。

首先用vue-cli webpack模板生成一个vue的项目,如下:

在确保全局安装了 vue-cli 的前提下,运行下面的命令

1
vue init webpack webpack_study

项目的文件结构

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
├── build/                      # webpack 配置文件
│ └── ...
├── config/
│ ├── index.js # 项目核心配置
│ └── ...
├── src/
│ ├── main.js # 程序入口文件
│ ├── App.vue # 程序入口vue组件
│ ├── components/ # 组件
│ │ └── ...
│ └── assets/ # 模块资源 (会被webpack处理)
│ └── ...
├── static/ # 纯静态资源 (直接拷贝到dist/static/里面)
├── test/
│ └── unit/ # 单元测试
│ │ ├── specs/ # 测试规范
│ │ ├── index.js # 测试入口文件
│ │ └── karma.conf.js # 测试运行配置文件
│ └── e2e/ # 端到端测试
│ │ ├── specs/ # 测试规范
│ │ ├── custom-assertions/ # 端到端测试自定义断言
│ │ ├── runner.js # 运行测试的脚本
│ │ └── nightwatch.conf.js # 运行测试的配置文件
├── .babelrc # babel 配置文件
├── .editorconfig # 编辑配置文件
├── .eslintrc.js # eslint 配置文件
├── index.html # index.html 入口模板文件
└── package.json # 运行的脚本与相关依赖

进入项目目录,打开package.json 查看其script,

1
2
3
4
5
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js"
},

开发环境中的配置

在运行 npm run dev 的时候可以发现,开发环境的入口文件时 build/dev-server.js

dev-server.js

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
// 检查node和npm的版本是否匹配,不匹配的话给出提示
require('./check-versions')()

var config = require('../config')
// 如果Node的process变量无法确定当前是开发环境还是生产环境,使用config/index.js下的dev中配置
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
// 跨平台的打开网址、文件等的包
// https://www.npmjs.com/package/opn
var opn = require('opn')

// node自带的path的模块
var path = require('path')
// express模块
var express = require('express')
var webpack = require('webpack')
// http代理的中间件
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')

// 设置服务器运行的端口号
var port = process.env.PORT || config.dev.port
// 是否开启自动打开浏览器,在config/index.js的dev中配置
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// config/index.js dev中的proxyTable定义了与后端api的代理配置
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable

// 启动express的服务
var app = express()
// 使用webpack.dev.conf中配置启动webpack编译
var compiler = webpack(webpackConfig)

// 启动webpack-dev-middleware中间件,将编译后的文件写入内存
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
// 启动webpack-dev-middleware中间件,也就是hot-reload功能
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {},
heartbeat: 2000
})

// html-webpack-plugin模板变化后强刷
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})

// 应用代理配置中间件
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})

// 应用connect-history-api-fallback,匹配资源
app.use(require('connect-history-api-fallback')())

// 应用devMiddleware中间件
app.use(devMiddleware)

// 应用hotMiddleware中间件
app.use(hotMiddleware)

// 设置静态资源路径
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:' + port

var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})

// 启动express的服务,并监听
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})

var server = app.listen(port)

module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}

综上,可以发现在dev-server.js 中主要做了这几件事情:

  • 获取webpack开发环境的配置文件,并启用webpack编译,获得编译对象compiler
  • 使用webpack-dev-middleware中间件,将编译后的文件写入内存
  • 使用webpack-hot-middleware开启hot-reload功能
  • 启动webpack的服务,并将上述中间件挂在express的服务上

webpack模板中并没有使用webpack-dev-server作为开发环境的服务器,而是启动了express的服务并应用了webpack-dev-middleware和webpack-hot-middleware中间件。两种方式实现的功能是一样的,webpack-dev-server的本质也是一个轻量级的express服务,稍后会有webpack-dev-server的原理解析。

webpack.dev.conf.js

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
// 工具函数集合
var utils = require('./utils')
var webpack = require('webpack')
// 配置文件
var config = require('../config')
// webpack配置合并插件
var merge = require('webpack-merge')
// webpack基本配置
var baseWebpackConfig = require('./webpack.base.conf')
// 自动生成html并且注入到.html文件中的插件
var HtmlWebpackPlugin = require('html-webpack-plugin')
// webpack错误信息提示插件
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

// hot-reload 需要在entry中添加一个入口文件,用于客户端与服务端的socket通信
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

module.exports = merge(baseWebpackConfig, {
module: {
// 关于样式的一些loader
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// 采用不同形式的source-map
devtool: '#cheap-module-eval-source-map',
// 添加编译过程中使用的插件
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
]
})

在webpack.dev.conf.js中定义了只有在dev环境下用到的一些配置,包括sourcemap以及dev环境下常用的插件集合。在此基础上,通过引入webpack.base.conf.js,将两者merge。

webpack.base.conf.js

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
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
return path.join(__dirname, '..', dir)
}

module.exports = {
entry: {
app: './src/main.js'
},
output: {
// 编译结束后输入的静态资源目标路径
path: config.build.assetsRoot,
// 对应的bundle的文件名
filename: '[name].js',
// 正式发布环境下编译输出的线上路径的根路径
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
// 后缀名自动补全
extensions: ['.js', '.vue', '.json'],
// 别名
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
// 各种处理模块的loader
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}

webpack.base.conf.js中定义了webpack编译的一些公共配置。

config/index.js中定义了生产环境以及开发环境的配置文件,主要是根据该文件,生成webpack配置文件供编译时使用。比如端口号、代理配置等

util.js主要定义了几个工具函数,包括生成静态资源路径,生成styleLoaders的配置等。

生产环境中的配置

执行 npm run build的时候,可以发现生产环境的入口是build/build.js

build/build.js

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
require('./check-versions')()

process.env.NODE_ENV = 'production'

// 终端显示loading效果的工具
var ora = require('ora')
// node中执行 rm -rf的工具
var rm = require('rimraf')
var path = require('path')
// 带颜色的输出
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
// 生产环境下的webpack配置文件
var webpackConfig = require('./webpack.prod.conf')

// 在命令行显示loading效果
var spinner = ora('building for production...')
spinner.start()

// 删除文件
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
// 带回调的webpack函数,编译完后调用该callback
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')

console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})

build/build.js主要完成了删除之前的文件,执行编译动作,将文件写入指定文件位置,并在控制台上输出信息的任务。

webpack.prod.conf.js中定义了在执行build命令时webpack的一些配置。与webpack.dev.conf.js的主要差别在于一些插件的使用。比如在prod环境下添加了一些压缩文件、提取公共文件、复制静态资源一类的插件。这些都是在生产环境下才会应用的一些配置。

总结

通读vue-cli webpack模板的配置可以发现,其配置文件主要分为了两部分,一是dev环境下,二是prod环境下的配置文件。在此基础还抽离出了不同环境下公共的webpack.base.conf.js。再者config.js抽离了两环境下的一些配置,这样用户就不用去具体的webpack的配置文件中去设置相关配置,只需在config.js中即可完成。util.js提供了相关的工具函数,用于更简便的输出webpack配置文件。

参考文章:

vue-cli webpack模板中文文档

vue-cli#2.0 webpack 配置分析

vue-cli webpack配置分析