作为 Vue 的使用者我们对于 vue-cli 都很熟悉,但是对它的 webpack 配置我们可能关注甚少,今天我们为大家带来 vue-cli#2.0 的 webpack 配置分析
vue-cli 的简介、安装我们不在这里赘述,对它还不熟悉的同学可以直接访问 vue-cli 查看
01 | .├── README.md |
02 | ├── build |
03 | │ ├── build.js |
04 | │ ├── check-versions.js |
05 | │ ├── dev-client.js |
06 | │ ├── dev-server.js |
07 | │ ├── utils.js |
08 | │ ├── webpack.base.conf.js |
09 | │ ├── webpack.dev.conf.js |
10 | │ └── webpack.prod.conf.js |
11 | ├── config |
12 | │ ├── dev.env.js |
13 | │ ├── index.js |
14 | │ └── prod.env.js |
15 | ├── index.html |
16 | ├── package.json |
17 | ├── src |
18 | │ ├── App.vue |
19 | │ ├── assets |
20 | │ │ └── logo.png |
21 | │ ├── components |
22 | │ │ └── Hello.vue |
23 | │ └── main.js |
24 | └── static |
本篇文章的主要关注点在
build - 编译任务的代码
config - webpack 的配置文件
package.json - 项目的基本信息
从 package.json 中我们可以看到
1 | "scripts" : { |
2 | "dev" : "node build/dev-server.js" , |
3 | "build" : "node build/build.js" , |
4 | "lint" : "eslint --ext .js,.vue src" |
5 | } |
当我们执行 npm run dev / npm run build 时运行的是 node build/dev-server.js 或 node build/build.js
让我们先从 build/dev-server.js 入手
01 | require( './check-versions' )() // 检查 Node 和 npm 版本 |
02 | var config = require( '../config' ) // 获取 config/index.js 的默认配置 |
03 |
04 | /* |
05 | ** 如果 Node 的环境无法判断当前是 dev / product 环境 |
06 | ** 使用 config.dev.env.NODE_ENV 作为当前的环境 |
07 | */ |
08 |
09 | if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) |
10 | var path = require( 'path' ) // 使用 NodeJS 自带的文件路径工具 |
11 | var express = require( 'express' ) // 使用 express |
12 | var webpack = require( 'webpack' ) // 使用 webpack |
13 | var opn = require( 'opn' ) // 一个可以强制打开浏览器并跳转到指定 url 的插件 |
14 | var proxyMiddleware = require( 'http-proxy-middleware' ) // 使用 proxyTable |
15 | var webpackConfig = require( './webpack.dev.conf' ) // 使用 dev 环境的 webpack 配置 |
16 |
17 | // default port where dev server listens for incoming traffic |
18 | /* 如果没有指定运行端口,使用 config.dev.port 作为运行端口 */ |
19 | var port = process.env.PORT || config.dev.port |
20 | // Define HTTP proxies to your custom API backend |
22 | /* 使用 config.dev.proxyTable 的配置作为 proxyTable 的代理配置 */ |
23 | var proxyTable = config.dev.proxyTable |
24 |
25 | /* 使用 express 启动一个服务 */ |
26 | var app = express() |
27 | var compiler = webpack(webpackConfig) // 启动 webpack 进行编译 |
28 |
29 | /* 启动 webpack-dev-middleware,将 编译后的文件暂存到内存中 */ |
30 | var devMiddleware = require( 'webpack-dev-middleware' )(compiler, { |
31 | publicPath: webpackConfig.output.publicPath, |
32 | stats: { |
33 | colors: true , |
34 | chunks: false |
35 | } |
36 | }) |
37 |
38 | /* 启动 webpack-hot-middleware,也就是我们常说的 Hot-reload */ |
39 | var hotMiddleware = require( 'webpack-hot-middleware' )(compiler) |
40 | // force page reload when html-webpack-plugin template changes |
41 | compiler.plugin( 'compilation' , function (compilation) { |
42 | compilation.plugin( 'html-webpack-plugin-after-emit' , function (data, cb) { |
43 | hotMiddleware.publish({ action: 'reload' }) |
44 | cb() |
45 | }) |
46 | }) |
47 |
48 | // proxy api requests |
49 | // 将 proxyTable 中的请求配置挂在到启动的 express 服务上 |
50 | Object.keys(proxyTable).forEach( function (context) { |
51 | var options = proxyTable[context] |
52 | if ( typeof options === 'string' ) { |
53 | options = { target: options } |
54 | } |
55 | app.use(proxyMiddleware(context, options)) |
56 | }) |
57 |
58 | // handle fallback for HTML5 history API |
59 | // 使用 connect-history-api-fallback 匹配资源,如果不匹配就可以重定向到指定地址 |
60 | app.use(require( 'connect-history-api-fallback' )()) |
61 |
62 | // serve webpack bundle output |
63 | // 将暂存到内存中的 webpack 编译后的文件挂在到 express 服务上 |
64 | app.use(devMiddleware) |
65 |
66 | // enable hot-reload and state-preserving |
67 | // compilation error display |
68 | // 将 Hot-reload 挂在到 express 服务上 |
69 | app.use(hotMiddleware) |
70 |
71 | // serve pure static assets |
72 | // 拼接 static 文件夹的静态资源路径 |
73 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) |
74 | // 为静态资源提供响应服务 |
75 | app.use(staticPath, express.static( './static' )) |
76 |
77 | // 让我们这个 express 服务监听 port 的请求,并且将此服务作为 dev-server.js 的接口暴露 |
78 | module.exports = app.listen(port, function (err) { |
79 | if (err) { |
80 | console.log(err) |
81 | return |
82 | } |
83 | var uri = '<a href="http://localhost' ">http://localhost:'</a> + port |
84 | console.log( 'Listening at ' + uri + '\n' ) |
85 |
86 | // when env is testing, don't need open it |
87 | // 如果不是测试环境,自动打开浏览器并跳到我们的开发地址 |
88 | if (process.env.NODE_ENV !== 'testing' ) { |
89 | opn(uri) |
90 | } |
91 | }) |
刚刚我们在 dev-server.js 中用到了 webpack.dev.conf.js 和 index.js,我们先来看一下 webpack.dev.conf.js
01 | var config = require( '../config' ) // 同样的使用了 config/index.js |
02 | var webpack = require( 'webpack' ) // 使用 webpack |
03 | var merge = require( 'webpack-merge' ) // 使用 webpack 配置合并插件 |
04 | var utils = require( './utils' ) // 使用一些小工具 |
05 | var baseWebpackConfig = require( './webpack.base.conf' ) // 加载 webpack.base.conf |
06 | /* 使用 html-webpack-plugin 插件,这个插件可以帮我们自动生成 html 并且注入到 .html 文件中 */ |
07 | var HtmlWebpackPlugin = require( 'html-webpack-plugin' ) |
08 |
09 | // add hot-reload related code to entry chunks |
10 | // 将 Hol-reload 相对路径添加到 webpack.base.conf 的 对应 entry 前 |
11 | Object.keys(baseWebpackConfig.entry).forEach( function (name) { |
12 | baseWebpackConfig.entry[name] = [ './build/dev-client' ].concat(baseWebpackConfig.entry[name]) |
13 | }) |
14 |
15 | /* 将我们 webpack.dev.conf.js 的配置和 webpack.base.conf.js 的配置合并 */ |
16 | module.exports = merge(baseWebpackConfig, { |
17 | module: { |
18 | // 使用 styleLoaders |
19 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) |
20 | }, |
21 | // eval-source-map is faster for development |
22 | // 使用 #eval-source-map 模式作为开发工具,此配置可参考 DDFE 往期文章详细了解 |
23 | devtool: '#eval-source-map' , |
24 | plugins: [ |
25 | /* definePlugin 接收字符串插入到代码当中, 所以你需要的话可以写上 JS 的字符串 */ |
26 | new webpack.DefinePlugin({ |
27 | 'process.env' : config.dev.env |
28 | }), |
29 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage |
30 | new webpack.optimize.OccurenceOrderPlugin(), |
31 | /* HotModule 插件在页面进行变更的时候只会重回对应的页面模块,不会重绘整个 html 文件 */ |
32 | new webpack.HotModuleReplacementPlugin(), |
33 | /* 使用了 NoErrorsPlugin 后页面中的报错不会阻塞,但是会在编译结束后报错 */ |
34 | new webpack.NoErrorsPlugin(), |
36 | /* 将 index.html 作为入口,注入 html 代码后生成 index.html文件 */ |
37 | new HtmlWebpackPlugin({ |
38 | filename: 'index.html' , |
39 | template: 'index.html' , |
40 | inject: true |
41 | }) |
42 | ] |
43 | }) |
我们看到在 webpack.dev.conf.js 中又引入了 webpack.base.conf.js, 它看起来很重要的样子,所以我们只能在下一章来看看 config/index.js 了 (摊手)
001 | var path = require( 'path' ) // 使用 NodeJS 自带的文件路径插件 |
002 | var config = require( '../config' ) // 引入 config/index.js |
003 | var utils = require( './utils' ) // 引入一些小工具 |
004 | var projectRoot = path.resolve(__dirname, '../' ) // 拼接我们的工作区路径为一个绝对路径 |
005 |
006 | /* 将 NodeJS 环境作为我们的编译环境 郑州网建*/ |
007 | var env = process.env.NODE_ENV |
008 | // check env & config/index.js to decide weither to enable CSS Sourcemaps for the |
009 | // various preprocessor loaders added to vue-loader at the end of this file |
010 | /* 是否在 dev 环境下开启 cssSourceMap ,在 config/index.js 中可配置 */ |
011 | var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) |
012 | /* 是否在 production 环境下开启 cssSourceMap ,在 config/index.js 中可配置 */ |
013 | var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) |
014 | /* 最终是否使用 cssSourceMap */ |
015 | var useCssSourceMap = cssSourceMapDev || cssSourceMapProd |
016 |
017 | module.exports = { |
018 | entry: { |
019 | app: './src/main.js' // 编译文件入口 |
020 | }, |
021 | output: { |
022 | path: config.build.assetsRoot, // 编译输出的静态资源根路径 |
023 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, // 正式发布环境下编译输出的上线路径的根路径 |
024 | filename: '[name].js' // 编译输出的文件名 |
025 | }, |
026 | resolve: { |
027 | // 自动补全的扩展名 |
028 | extensions: [ '' , '.js' , '.vue' ], |
029 | // 不进行自动补全或处理的文件或者文件夹 |
030 | fallback: [path.join(__dirname, '../node_modules' )], |
031 | alias: { |
032 | // 默认路径代理,例如 import Vue from 'vue',会自动到 'vue/dist/vue.common.js'中寻找 |
033 | 'vue$' : 'vue/dist/vue.common.js' , |
034 | 'src' : path.resolve(__dirname, '../src' ), |
035 | 'assets' : path.resolve(__dirname, '../src/assets' ), |
036 | 'components' : path.resolve(__dirname, '../src/components' ) |
037 | } |
038 | }, |
039 | resolveLoader: { |
040 | fallback: [path.join(__dirname, '../node_modules' )] |
041 | }, |
042 | module: { |
043 | preLoaders: [ |
044 | // 预处理的文件及使用的 loader |
045 | { |
046 | test: /\.vue$/, |
047 | loader: 'eslint' , |
048 | include: projectRoot, |
049 | exclude: /node_modules/ |
050 | }, |
051 | { |
052 | test: /\.js$/, |
053 | loader: 'eslint' , |
054 | include: projectRoot, |
055 | exclude: /node_modules/ |
056 | } |
057 | ], |
058 | loaders: [ |
059 | // 需要处理的文件及使用的 loader |
060 | { |
061 | test: /\.vue$/, |
062 | loader: 'vue' |
063 | }, |
064 | { |
065 | test: /\.js$/, |
066 | loader: 'babel' , |
067 | include: projectRoot, |
068 | exclude: /node_modules/ |
069 | }, |
070 | { |
071 | test: /\.json$/, |
072 | loader: 'json' |
073 | }, |
074 | { |
075 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, |
076 | loader: 'url' , |
077 | query: { |
078 | limit: 10000, |
079 | name: utils.assetsPath( 'img/[name].[<a href="hash:7">hash:7</a>].[ext]' ) |
080 | } |
081 | }, |
082 | { |
083 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, |
084 | loader: 'url' , |
085 | query: { |
086 | limit: 10000, |
087 | name: utils.assetsPath( 'fonts/[name].[<a href="hash:7">hash:7</a>].[ext]' ) |
088 | } |
089 | } |
090 | ] |
091 | }, |
092 | eslint: { |
093 | // eslint 代码检查配置工具 |
094 | formatter: require( 'eslint-friendly-formatter' ) |
095 | }, |
096 | vue: { |
097 | // .vue 文件配置 loader 及工具 (autoprefixer) |
098 | loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }), |
099 | postcss: [ |
100 | require( 'autoprefixer' )({ |
101 | browsers: [ 'last 2 versions' ] |
102 | }) |
103 | ] |
104 | } |
105 | } |
终于分析完了 webpack.base.conf.js,来让我们看一下 config/index.js
index.js 中有 dev 和 production 两种环境的配置
01 | // see http://vuejs-templates.github.io/webpack for documentation. |
02 | // 不再重复介绍了 ... |
03 | var path = require( 'path' ) |
04 |
05 | module.exports = { |
06 | build: { // production 环境 |
07 | env: require( './prod.env' ), // 使用 config/prod.env.js 中定义的编译环境 |
08 | index: path.resolve(__dirname, '../dist/index.html' ), // 编译输入的 index.html 文件 |
09 | assetsRoot: path.resolve(__dirname, '../dist' ), // 编译输出的静态资源路径 |
10 | assetsSubDirectory: 'static' , // 编译输出的二级目录 |
11 | assetsPublicPath: '/' , // 编译发布的根目录,可配置为资源服务器域名或 CDN 域名 |
12 | productionSourceMap: true , // 是否开启 cssSourceMap |
13 | // Gzip off by default as many popular static hosts such as |
14 | // Surge or Netlify already gzip all static assets for you. |
15 | // Before setting to `true`, make sure to: |
16 | // npm install --save-dev compression-webpack-plugin |
17 | productionGzip: false , // 是否开启 gzip |
18 | productionGzipExtensions: [ 'js' , 'css' ] // 需要使用 gzip 压缩的文件扩展名 |
19 | }, |
20 | dev: { // dev 环境 |
21 | env: require( './dev.env' ), // 使用 config/dev.env.js 中定义的编译环境 |
22 | port: 8080, // 运行测试页面的端口 |
23 | assetsSubDirectory: 'static' , // 编译输出的二级目录 |
24 | assetsPublicPath: '/' , // 编译发布的根目录,可配置为资源服务器域名或 CDN 域名 |
25 | proxyTable: {}, // 需要 proxyTable 代理的接口(可跨域) |
26 | // CSS Sourcemaps off by default because relative paths are "buggy" |
27 | // with this option, according to the CSS-Loader README |
28 | // (https://github.com/webpack/css-loader#sourcemaps) |
29 | // In our experience, they generally work as expected, |
30 | // just be aware of this issue when enabling this option. |
31 | cssSourceMap: false // 是否开启 cssSourceMap |
32 | } |
33 | } |
至此,我们的 npm run dev 命令就讲解完毕,下面让我们来看一看执行 npm run build 命令时发生了什么 ~
01 | // 看上边的链接 |
02 | require( './check-versions' )() // 检查 Node 和 npm 版本 |
03 | require( 'shelljs/global' ) // 使用了 shelljs 插件,可以让我们在 node 环境的 js 中使用 shell |
04 | env.NODE_ENV = 'production' |
05 |
06 | var path = require( 'path' ) // 不再赘述 |
07 | var config = require( '../config' ) // 加载 config.js |
08 | var ora = require( 'ora' ) // 一个很好看的 loading 插件 |
09 | var webpack = require( 'webpack' ) // 加载 webpack |
10 | var webpackConfig = require( './webpack.prod.conf' ) // 加载 webpack.prod.conf |
11 |
12 | console.log( // 输出提示信息 ~ 提示用户请在 http 服务下查看本页面,否则为空白页 |
13 | ' Tip:\n' + |
14 | ' Built files are meant to be served over an HTTP server.\n' + |
15 | ' Opening index.html over file:// won\'t work.\n' |
16 | ) |
17 |
18 | var spinner = ora( 'building for production...' ) // 使用 ora 打印出 loading + log |
19 | spinner.start() // 开始 loading 动画 |
20 |
21 | /* 拼接编译输出文件路径 */ |
22 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) |
23 | /* 删除这个文件夹 (递归删除) */ |
24 | rm( '-rf' , assetsPath) |
25 | /* 创建此文件夹 */ |
26 | mkdir( '-p' , assetsPath) |
27 | /* 复制 static 文件夹到我们的编译输出目录 */ |
28 | cp( '-R' , 'static/*' , assetsPath) |
29 |
30 | // 开始 webpack 的编译 |
31 | webpack(webpackConfig, function (err, stats) { |
32 | // 编译成功的回调函数 |
33 | spinner.stop() |
34 | if (err) throw err |
35 | process.stdout.write(stats.toString({ |
36 | colors: true , |
37 | modules: false , |
38 | children: false , |
39 | chunks: false , |
40 | chunkModules: false |
41 | }) + '\n' ) |
42 | }) |
001 | var path = require( 'path' ) // 不再赘述 |
002 | var config = require( '../config' ) // 加载 confi.index.js |
003 | var utils = require( './utils' ) // 使用一些小工具 |
004 | var webpack = require( 'webpack' ) // 加载 webpack |
005 | var merge = require( 'webpack-merge' ) // 加载 webpack 配置合并工具 |
006 | var baseWebpackConfig = require( './webpack.base.conf' ) // 加载 webpack.base.conf.js |
007 | /* 一个 webpack 扩展,可以提取一些代码并且将它们和文件分离开 */ |
008 | /* 如果我们想将 webpack 打包成一个文件 css js 分离开,那我们需要这个插件 */ |
009 | var ExtractTextPlugin = require( 'extract-text-webpack-plugin' ) |
010 | /* 一个可以插入 html 并且创建新的 .html 文件的插件 */ |
011 | var HtmlWebpackPlugin = require( 'html-webpack-plugin' ) |
012 | var env = config.build.env |
013 |
014 | /* 合并 webpack.base.conf.js */ |
015 | var webpackConfig = merge(baseWebpackConfig, { |
016 | module: { |
017 | /* 使用的 loader */ |
018 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) |
019 | }, |
020 | /* 是否使用 #source-map 开发工具,更多信息可以查看 DDFE 往期文章 */ |
021 | devtool: config.build.productionSourceMap ? '#source-map' : false , |
022 | output: { |
023 | /* 编译输出目录 */ |
024 | path: config.build.assetsRoot, |
025 | /* 编译输出文件名 */ |
026 | filename: utils.assetsPath( 'js/[name].[chunkhash].js' ), // 我们可以在 hash 后加 :6 决定使用几位 hash 值 |
027 | // 没有指定输出名的文件输出的文件名 |
028 | chunkFilename: utils.assetsPath( 'js/[id].[chunkhash].js' ) |
029 | }, |
030 | vue: { |
031 | /* 编译 .vue 文件时使用的 loader */ |
032 | loaders: utils.cssLoaders({ |
033 | sourceMap: config.build.productionSourceMap, |
034 | extract: true |
035 | }) |
036 | }, |
037 | plugins: [ |
038 | /* 使用的插件 */ |
040 | /* definePlugin 接收字符串插入到代码当中, 所以你需要的话可以写上 JS 的字符串 */ |
041 | new webpack.DefinePlugin({ |
042 | 'process.env' : env |
043 | }), |
044 | /* 压缩 js (同样可以压缩 css) */ |
045 | new webpack.optimize.UglifyJsPlugin({ |
046 | compress: { |
047 | warnings: false |
048 | } |
049 | }), |
050 | new webpack.optimize.OccurrenceOrderPlugin(), |
051 | // extract css into its own file |
052 | /* 将 css 文件分离出来 */ |
053 | new ExtractTextPlugin(utils.assetsPath( 'css/[name].[contenthash].css' )), |
054 | // generate dist index.html with correct asset hash for caching. |
055 | // you can customize output by editing /index.html |
057 | /* 输入输出的 .html 文件 */ |
058 | new HtmlWebpackPlugin({ |
059 | filename: config.build.index, |
060 | template: 'index.html' , |
061 | inject: true , // 是否注入 html |
062 | minify: { // 压缩的方式 |
063 | removeComments: true , |
064 | collapseWhitespace: true , |
065 | removeAttributeQuotes: true |
066 | // more options: |
067 | // https://github.com/kangax/html-minifier#options-quick-reference |
068 | }, |
069 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin |
070 | chunksSortMode: 'dependency' |
071 | }), |
072 | // split vendor js into its own file |
073 | /* 没有指定输出文件名的文件输出的静态文件名 */ |
074 | new webpack.optimize.CommonsChunkPlugin({ |
075 | name: 'vendor' , |
076 | minChunks: function (module, count) { |
077 | // any required modules inside node_modules are extracted to vendor |
078 | return ( |
079 | module.resource && |
080 | /\.js$/.test(module.resource) && |
081 | module.resource.indexOf( |
082 | path.join(__dirname, '../node_modules' ) |
083 | ) === 0 |
084 | ) |
085 | } |
086 | }), |
087 | // extract webpack runtime and module manifest to its own file in order to |
088 | // prevent vendor hash from being updated whenever app bundle is updated |
089 | /* 没有指定输出文件名的文件输出的静态文件名 */ |
090 | new webpack.optimize.CommonsChunkPlugin({ |
091 | name: 'manifest' , |
092 | chunks: [ 'vendor' ] |
093 | }) |
094 | ] |
095 | }) |
096 |
097 | /* 开启 gzip 的情况下使用下方的配置 */ |
098 | if (config.build.productionGzip) { |
099 | /* 加载 compression-webpack-plugin 插件 */ |
100 | var CompressionWebpackPlugin = require( 'compression-webpack-plugin' ) |
101 | /* 向webpackconfig.plugins中加入下方的插件 */ |
102 | webpackConfig.plugins.push( |
103 | /* 使用 compression-webpack-plugin 插件进行压缩 */ |
104 | new CompressionWebpackPlugin({ |
105 | asset: '[path].gz[query]' , |
106 | algorithm: 'gzip' , |
107 | test: new RegExp( |
108 | '\\.(' + |
109 | config.build.productionGzipExtensions.join( '|' ) + |
110 | ')$' |
111 | ), |
112 | threshold: 10240, |
113 | minRatio: 0.8 |
114 | }) |
115 | ) |
116 | } |
117 |
118 | module.exports = webpackConfig |
至此 ~ 我们的 vue-cli#2.0 webpack 配置分析文件就讲解完毕 ~
对于一些插件的详细 options 我们没有进行讲解,感兴趣的同学可以去 npm 商店搜索对应插件查看 options ~