logo头像

王者风范 自由洒脱

vue-cli 配置分析

本文于1046天之前发表,文中内容可能已经过时。

vue-cli#2.0 webpack 配置分析

目录结构

├── README.md
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── index.html
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── Hello.vue
│ └── main.js
└── static
该笔记主要关注:

  • build - 编译任务的代码
  • config - webpack的配置文件
  • package.json - 项目的基本信息

入口

从package.json中能看到

1
2
3
4
5
"scripts":{
"dev":"node build/dev-server.js",
"build":"node build/build.js",
"lint":"eslint --ext .js,.vue src"
}

执行npm run dev和npm run build时运行的其实就是node build
/dev-server.js和node build/build.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
102
103
104
105
106
107
108
//检查Node和npm版本
require('./check-versions')()

//获取config/index.js的默认配置
var config = require('../config')

//如果Node的环境无法判断当前是dev/product环境
//则使用config.dev.nev.NODE_ENV作为当前的环境
if(!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)

//使用NodeJS自带的文件路径工具
var path = require('path')

//使用express
var express = require('express')

//使用webpack
var webpack = require('webpack')

//一个可以强制打开浏览器并跳转到指定url的插件
var opn = require('opn')

//使用proxyTable
var proxyMiddleware = require('http-proxy-middleware')

//使用dev环境的webpack配置
var webpackConfig = require('./webpack.dev.conf')

//default port where dev server listens for incoming traffic

//如果没有指定运行端口,使用config.dev.port作为运行端口
var port = process.env.PORT || config.dev.port

//Define HTTP proxies to your custom API backend
//https://github.com/chimurai/http-proxy-middleware

//使用config.dev.proxyTable的配置作为proxyTable的代理配置
var proxyTable = config.dev.proxyTable

//使用express启动一个服务
var app = express()

//启动webpack进行编译
var compiler = webpack(webpackConfig)

//启动webpack-dev-middleware,将编译后的文件暂存到内存中
var devMiddleware = require('webpack-dev-middleware')(compiler,{
publicPath:webpackConfig.output.publicPath,
stats:{
colors:true,
chunks:false
}
})

//启动webpack-hotmiddleware,即Hot-reload
var hotMiddleware = require('webpack-hot-middlerware')(compiler)
//force page reload when html-webpack-plugin template changes
compiler.plugin('compilation',function(compilation){
compilation.plugin('html-webpack-plugin-after-emit',function(data,cb){
hotMiddlerware.publish({action:'reload'})
cb()
})
})

//proxy api requests
//将proxyTable中的请求配置挂载到启动的express服务上
Object.keys(proxyTable).forEach(function(context){
var options = proxyTable[context]
if (typeof options === 'string'){
options = {target:options}
}
app.use(proxyMiddleware(context,options))
})

//handle fallback for HTML5 history API
//使用connect-history-api-fallback匹配资源,如果不匹配就可以重定向到指定地址
app.use(require('connect-history-api-fallback')())

//serve webpack bundle output
//将暂存到内存中的webpack编译后的文件挂载到express服务上
app.use(devMiddleware)

//enable hot-reload and state-preserving
//compilation error display
//将Hot-reload挂载到express服务上
app.use(hotMiddleware)

//serve pure static assets
//拼接static文件夹的静态资源路径
var staticPath = path.posix.join(config.dev.assetsPublicPath,config.dev.assetsSubDirectory)
//为静态资源提供响应服务
app.use(staticPath,express.static('./static'))

//让搭起的express服务监听port的请求,并且将此服务作为dev-server.js的接口暴露
module.exports = app.listen(port,function(err){
if(err){
console.log(err)
return
}
var uri = 'http://localhost:'+port
console.log('Listening at '+ uri + '\n')

//when env is testing,don't need open it
//如果不是测试环境,自动打开浏览器并跳到我们的开发地址
if(process.env.NODE_ENV !== 'testing'){
opn(uri)
}
})

webpack.dev.conf.js

上面的dev-server.js用到了webpack.dev.conf.js和index.js,先来解析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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//同样的使用了config/index.js
var config = require('../config')

//使用webpack
var webapck = require('webpack')

//使用webpack配置合并插件
var merge = require('webpack-merge')

//使用一些小工具
var utils = require('./utils')

//加载webpack.base.conf

var baseWebpackConfig = require('./webpack.base.conf')

//使用html-webpack-plugin插件,该插件可以帮我们自动生成html并注入到.html文件中
var HtmlWebpackPlugin = require('html-webpack-plugin')

//add hot-reload related code to entey chunks
//将Hot-reload相对路径添加到webpack.base.conf的对应entry前
Object.keys(baseWebpackConfig.entry).forEach(function(name){
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

//将我们webpack.dev.conf.js的配置和webpack.base.conf.js的配置合并
module.exports = merge(baseWebpackConfig,{
module:{
//使用styleLoaders
loaders:utils.sytleLoaders({sourceMap:config.dev.cssSourceMap})
},
//eval-source-map is faster for development
//使用eval-source-map模式作为开发工具,
devtool:'#eval-source-map',
plugins:[
//definePlugin接收字符串插入到代码中,所以需要的话可以写上JS的字符串
new webpack.DefinePlugin({
'process.env':config.dev.env
}),
//https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.optimize.OccurenceOrderPlugin(),

//HotModule插件在页面进行变更的时候只会重绘对应的页面模块,不会重绘整个html文件
new webpack.HotModuleReplacementPlugin(),

//使用了NoErrorsPlugin后,页面中的报错不会阻塞,但是会在编译结束后报错
new webpack.NoErrorsPlugin(),
//https://github.com/ampedandwired/html-webpack-plugin

//将index.html作为入口,诸如html代码后生成index.html文件
new HtmlWebpackPlugin({
filename:'index.html',
template:'index.html',
inject:ture
})
]
})

webpack.base.conf.js

上面webpack.dev.conf.js中又引入了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
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
//使用NodeJS自带的文件路径插件
var path = require('path')

//引入config/index.js
var config = require('../config')

//引入一些小工具
var utils = require('./utils')

//拼接我们的工作区路径为一个绝对路径
var projectRoot = path.resolve(__dirname,'../')

//将NodeJS环境作为我们的编译环境
var env = process.env.NODE_ENV

//check env & config/index.js to enable CSS Sourcemaps for the
//various preprocessor loaders added to vue-loader at the end of this file
//是否在dev环境下开启cssSourceMap,在config/index.js中可配置
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)

//是否在production环境下开启cssSourceMap,在config/index.js中可配置
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
//最终是否使用cssSourceMap
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd

module.exports = {
entry:{
//编译文件入口
app:'./src/main.js'
},
output:{
//编译输出的根路径
path:config.build.assetsRoot,
//正式发布环境下编译输出的发布路径
publicPath:process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
//编译输出的文件名
filename:'[name].js'
},
resolve:{
//自动补全的扩展名
extensions:['','.js','.vue'],
//不进行自动补全或处理的文件或文件夹
fallback:[path.join(__dirname,'../node_modules')],
alias:{
//默认路径代理,例如import Vue from 'vue',会自动到'vue/dist/vue.common.js'中寻找
'vue':'vue/dist/vue.common.js',
'src':path.resolve(__dirname,'../src'),
'assets':path.resolve(__dirname,'../src/assets'),
'components':path.resolve(__dirname,'../src/components')
}
},
resolveLoader:{
fallback:[path.join(__dirname,'../node_modules')]
},
module:{
preLoaders:[
//预处理的文件及使用的loader
{
test:/\.vue$/,
loader:'selint',
include:projectRoot,
exclude:/node_modules/
},
{
test: /\.js$/,
loader: 'eslint',
include: projectRoot,
exclude: /node_modules/
}
],
loaders:[
//需要处理的文件及使用的loader
{
test:/\.vue$,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel',
include: projectRoot,
exclude: /node_modules/
},
{
test: /\.json$/,
loader: 'json'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url',
query: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url',
query: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
eslint:{
//eslint代码检查配置工具
formatter:require('eslint-friendly-formatter')
},
vue:{
//.vue文件配置loader即工具(autoprefixer)
loaders:utils.cssLoader({sourceMap:useCssSourceMap}),
postcss:[
require('autoprefixer')({
browers:['last 2 versions']
})
]
}
}

config/index.js

最后再来看下config/index.js
index.js中有dev和production两种环境的配置

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
//see http://vuejs-templates.github.io/webpack for documentation
//路径
var path = require('path')

module.exports = {
//production 环境
build:{
//使用config/prod.env.js中定义的编译环境
env:require('./prod.env'),
//编译输入的index.html文件
index:path.resolve(__dirname,'../dist/index.html'),
//编译输出的静态资源根路径
assetsRoot:path.resolve(__dirname,'../dist'),
//编译输出的二级目录
assetsSubDirectory:'static',
//编译发布上线路径的根目录,可配置为资源服务器域名或CDN域名
assetsPublicPath:'/',
//是否开启cssSourceMap
productionSourceMap:true,
//是否开启gzip
productionGzip:false,
//需要使用gzip压缩的文件扩展名
productionGzipExtensions:['js','css']
},
//dev环境
dev:{
//使用config/dev.env.js中定义的编译环境
env:reauire('./dev.env'),
//运行测试页面的端口
port:8080,
//编译输出的二级目录
assetsSubDirectory:'static',
//编译发布上线路径的根目录,可配置为资源服务器域名或CDN域名
assetsPublicPath:'/',
//需要proxyTable代理的接口(可跨域)
proxyTable:{},
//是否开启cssSourceMap
cssSourceMap:false
}
}

至此关于npm run dev命令分析完毕
再来看看npm run 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
43
44
45
46
47
48
49
//https://github.com/shelljs/shelljs

//检查Node和npm版本
require('./check-versions')()

//使用了shelljs插件,可以让我们在node环境的js中使用shell
require('shelljs/global')
env.NODE_ENV = 'production'

var path = require('path')
var config = require('../config')

//一个很好看的loading插件 啊???
var ora = require('ora')

//加载webpack
var webpack = require('webpack')

//加载webpack.prod.conf
var webpackConfig = require('./webpack.prod.conf')

//使用ora打印出loading+log
var spinner = ora('building for production...')
//开始loading动画
spinner.start()

//拼接编译输出文件路径
var assetsPath = path.join(config.build.assetsRoot,config.build.assetsSubDirectory)

//删除文件夹(递归删除)
rm('-rf',assetsPath)
//创建此文件夹
mkdir('-p',assetsPath)
//赋值static文件夹到我们的编译输出目录
cp('-R','static/*',assetsPath)

//开始webpack的编译
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')
})

webpack.prod.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
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
var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
//加载webpack配置合并工具
var merge = require('webpack-merge')

//加载webpack.base.conf.js
var baseWebpackConfig = require('./webpack.base.conf')
//一个webpack扩展,可以提供一些代码并且将它们和文件分离开
//如果想将webpack打包成一个文件css js分隔开,那需要下面这个插件
var ExtractTextPlugin = require('extract-text-webpack-plugin')

//一个可以插入html并且创建新的.html文件的插件
var HtmlWebpackPlugin = require('html-webpack-plugin')
var env = config.build.env

//合并webpack.base.conf.js
var webpackConfig = merge(baseWebpackConfig,{
module:{
//使用的loader
loaders:utils.styleLoaders({
sourceMap:config.build.productionSourceMap,extract:true
})
},
//是否使用#source-map开发工具
devtool:config.build.productionSourceMap ?
'#source-map':false,
output:{
//编译输出目录
path: config.build.assetsRoot,
//编译输出文件名
//可以在hash后加:6决定使用几位hash值
filename:utils.assetsPath('js/[name].[chunkhash].js'),
//没有指定输出名的文件输出的文件名
chunkFilename:utils.assetsPath('js/[id].[chunkhash].js')
},
vue:{
//编译.vue文件时使用的loader
loaders:utils.cssLoaders({
sourceMap:config.build.productionSourceMap,
extract:true
})
},
plugins:[
//使用的插件
//https://vuejs.github.io/vue-loader/en/workflow/production.html
//definePlugin 接收字符串插入到代码当中,所以你需要的话可以写上JS的字符串
new webpack.DefinePlugin({
'process.env':env
}),
//压缩js(同样可也压缩css)
new webpack.optimize.UglifyJsPlugin(),
//extract css into its own ifle
//将css文件分离出来
new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
//输入输出的.html文件
//see https://github.com/ampedandwied/html-webpack-plugin
new HtmlWebpackPlugin({
filename:config.build.index,
template:'index.html',
//是否注入html
inject:true,
//压缩的方式
minify:{
removeComments:true,
collapseWhitespace:true,
removeAttributeQuotes:true
//more options:
//https://github.com/kangx/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode:'dependency'
}),
//split vendor js into its own file
//没有指定输出文件夹的文件输出地静态文件名
new webpack.optmize.CommonsChunkPlugin({
name:'vendor',
minChunks:function(module,count){
//any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname,'../node_modules')
) === 0
)
}
}),
//没有指定输出文件名的文件输出的静态文件名
new webpack.optimize.CommonsChunkPlugin({
name:'manifest',
chunks:['vendor']
})
]
})

//开启gzip的情况下使用下方配置
if(config.build.productionGzip){
//加载compression-webpack-plugin插件
var CompressionWebpackPlugin = require('compression-webpack-plugin')
//向webpackconfig.plugins中加入下方的插件
var reProductionGzipExtensions = '\\.('+config.build.productionGzipExtensions.join('|')+'$)'
webpackConfig.plugins.push(
//使用compression-webpack-plugin插件进行压缩
new CompressionWebpackPlugin({
asset:'[path].gz[query]',
algorithm:'gzip',
test:new RegExp(reProductionGzipExtensions),//注:此处因有代码格式化的bug,与源码有差异
threshold:10240,
minRatio:0.8
})
)
}

module.exports = webpackConfig

完事儿,为了加深印象全程纯手打,累死我了…