[万字逐步详解]使用 webpack 打包 vue 项目(优化生产环境)
之前在 [万字逐步详解]使用 webpack 打包 vue 项目(基础生产环境) 中比较详尽的手把手带着过了一遍 production 环境的部署,以及在 [万字逐步详解]使用 webpack-dev-server + ESLint 配置 vue 项目的开发环境 中过了一遍使用 webpack-dev-server 配置开发环境,以及使用 ESLint 去提高代码质量的过程。
webpack 配置最后一节就讲一下怎么提高生产环境中打包出来代码的质量。这里会从两个点进行配置的优化:
- 环境的分离
原本只有一个 webpack.config.js
,但是通过配置开发环境和生产环境,已经很明显的感受到二者的差异,所以有必要对不同的环境进行分离。
- 功能的优化
这些包括对 tree shaking 的优化之类的——虽然这些优化在生产环境下默认开启。以及不默认开启的代码分割、魔法注释、CSS 文件的处理,和对文件添加 hash 值。
分离环境
是时候将 webpack 的 生产环境 和 开发环境 分离开来了。从运行结果来说,原本打包的代码可能只有 100KB+,但是配上 Source Map 之后已经到了 1M 多。并且,生产环境并不需要 dev server,也不需要 Source Map,这些冗余的代码无异于会让上线的代码变得更“重”,从而影响访问的效率。
注*:还有一个选择是使用环境变量去判断,然后在同一个文件内对配置进行修改,这个做法也很简单,如:
module.exports = (env, argv) => {
const config = {
// ...省略众多基础
};
if (env === 'production') {
config.mode = 'production';
config.devtool = false;
config.plugins = [
// 展开操作符
...config.plugins,
// 新的 plugins
];
}
return config;
};
这里选择的方式是通过分割不同的配置文件,去对不同的环境进行适配。这种情况下,会有一个开发配置,生产配置,和公共配置,三个配置文件。
环境分析
结合上上一篇的 production build 和上一篇的 开发环境 配置的过程中,已经发现了两个环境的差异越来越大了。
其原因是除了编译是开发和生产环境都需要的功能之外,其他的功能开发与生产之间的需求差异挺大的——生产环境下需要尽可能减少代码的尺寸,会额外增添一些 loaders 和 plugins 去减少打包尺寸。而开发环境下需要多次编译,需求是尽可能少的对文件进行处理,提高编译速度。
另外,生产环境下会使用 webpack-dev-server 去生成一个本地服务器,同时为了减少编译速度而开启模块热更新,为了方便 debug 而开启 source map 功能。而生产环境下肯定会开启服务器去 host 网站,所以对 webpack-dev-server 没有什么需求,同样也不会需要 source map 和热更新功能。
简单的列举一个对比,去更直观的了解一下两个环境的差异:
功能 | 开发 | 生产 |
---|---|---|
webpack & webpack-cli | √ | √ |
vue-loader & vue-template-compiler | √ | √ |
style-loader & less & less-loader | √ | √ |
file-loader & url-loader | √ | √ |
htmlWebpackPlugin | √ | √ |
clean-webpack-plugin | × | √ |
copy-webpack-plugin | × | √ |
webpack-dev-server | √ | × |
Source Map | √ | × |
HRM | √ | × |
ESLint | √ | × |
tree shaking | × | √ |
code splitting/代码分割 | × | √ |
下文列举的其他针对开发环境优化 | × | √ |
接下来就会将双方都共用的功能,提取到共用配置(common)中,再根据需求去配置开发配置(dev),和生产配置(production)。
共用配置
首先,上面的表格所列举 loaders,也就是开发环境和生产环境都打勾的,是两个环境都需要的,毕竟需要依赖这些 loaders 去解析文件。同理,vue-loader-plugin 和 html-webpack-plugin,这两个也是必须同时作用于两个环境上。
再有,target 和 entry 也是一致的,毕竟这是一个基于 Vue 的 SPA。
至于 output,这个就看项目了,有些项目会分别在 production 和 development 指定不同的文件夹去进行编译,但是这里为了方便起见,用同一个 output 问题也不大。开发环境下使用的 webpack-dev-server 会将文件写入缓存中,不会实际从 output 中提取资源,这也是为什么可以偷懒的原因。
所以 webpack.common.js 的配置就是下面这样的:
const path = require('path');
const webpack = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
target: 'web',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
// 解析Vue文件
{ test: /\.vue$/, loader: 'vue-loader' },
// 它会应用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 块
{
test: /\.js$/,
loader: 'babel-loader',
},
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `<style>` 块
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'],
},
// 对 less 文件进行处理
{
test: /\.less$/i,
use: [
// compiles Less to CSS
'style-loader',
'css-loader',
'less-loader',
],
},
// url-loader,针对图片的优化
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 这个上限是官方文档设立的,这里就不改了
limit: 8192,
// 同理,不设置为false打开的会是 esModule
esModule: false,
},
},
],
},
],
},
plugins: [
// 设置 webpack 所要的基础配置
new webpack.DefinePlugin({
BASE_URL: JSON.stringify('./'),
}),
// 请确保引入这个插件!
new VueLoaderPlugin(),
// 空的狗欧早寒素仅会生成一个 index.html,会引入合适的文件,但是入口DOM节点 app 不见了,所以需要其他的配置
new HtmlWebpackPlugin({
template: 'public/index.html',
}),
],
};
生产配置
因为需要覆盖掉 plugins 的一些属性,这里会使用一个名为 webpack-merge 的依赖包去 merge 插件的数组。它的优势在于可以让开发者专注编写对应环境所需要的配置即可,这个插件自己内部会完成不同配置之间的 merge。
1、安装 webpack-merge
注*:使用 webpack-merge 也要注意版本之间的区别,用法可能会有些微不同。
安装方式依旧是用 npm:
D:\vue-webpack>npm i -D
webpack-merge
2、新建/修改 webpack.prod.js
这就是生产模式的代码,主要新增了 mode,以及将原本没有的插件加了回来
完整源码如下:
const common = require('./webpack.common');
const { merge } = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyPlugin({
patterns: [{ from: 'public/favicon.ico', to: './' }],
}),
],
});
3、修改 package.json
这里只要修改一行代码即可,就是找到 script 下面的那部分,将正确的 webpack 配置文件路放到命令中去:
{
// 其它不变
"scripts": {
"build": "webpack --config webpack.prod.js"
}
}
4、运行测试没有问题
开发配置
这里主要就是配置一下 devtool 和 devServer 的配置,也是使用 webpack-merge 去进行操作
1、新建/修改 webpack.dev.js
const path = require('path');
const common = require('./webpack.common');
const { merge } = require('webpack-merge');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
open: true,
// 指定端口
port: 9000,
// 命令行中会显示打包的进度
progress: true,
// 开启热更新
hot: true,
// 是个很有趣的特性
historyApiFallback: true,
// 添加静态资源的引用,可以用数组
// 这样原本没有被打包进去的内容也可以正确被引用
contentBase: path.join(__dirname, 'public'),
},
});
2、修改 package.json
方法一样
{
// 其它不变
"scripts": {
"serve": "webpack serve --config webpack.dev.js"
}
}
3、同样别忘了测试
最后一步就是删除多余的 webpack.config.js,也就是最初建立的配置文件。
功能优化
一些可以提高 webpack 打包性能的配置,具体的链接可以查看 Tree Shaking,目前下面列举的属性在生产环境下都是默认开启的。
webpack 在官网上列举了几个开发环境下配置没什么意义的插件/功能,这里列举一下,将来避免在开发环境使用:
- TerserPlugin
- [fullhash]/[chunkhash]/[contenthash]
- AggressiveSplittingPlugin
- AggressiveMergingPlugin
- ModuleConcatenationPlugin
不导出死代码
会在 production 模式下自动开启,它不会将未被引用的 dead code 打包近项目中,这个功能又称 tree shaking
在其他环境下,可以通过 optimization 进行配置:
modules.export = {
// 其他配置不变
optimization: {
// 字面意思就是:只 export 使用的代码
usedExports: true,
},
};
其他模式下增添代码压缩功能
有需要的话——例如说新增配置与 production 相比更像 development 的 uat 环境之类的——可以使用,实现方法如下:
modules.export = {
// 其他配置不变
optimization: {
// 其他配置不变
// 最小化
minimize: true,
},
};
合并模块
即 scope hoisting 功能,这是一个从 webpack3 开始就推出来的一个特性,可以将几个模块打包到一个文件中去,从而进一步减少文件打包的体积。这个功能在生产环境下默认开启,在其他环境下的开启方法如下:
modules.export = {
// 其他配置不变
optimization: {
// 其他配置不变
// 合并模块
// 设置为 false 可以重写 mode: production 中的默认设置
concatenateModules: true,
},
};
在 Vue 的项目中,应该会另外实现对于 concatenateModules 功能。一来打包出来的代码其实还是在一个模块里的,二来毕竟这个功能只能用于 ES6 模块,以下是来自 webpack 的说明:
Keep in mind that this plugin will only be applied to ES6 modules processed directly by webpack. When using a transpiler, you’ll need to disable module processing (e.g. the modules option in Babel)
大意为:
注意这个模块只会被应用于被 webpack 直接处理的 ES6 模块。当使用编译器器是,需要禁用模块处理,如:babel 中的 modules 功能
而 Vue 的代码肯定是要通过 babel 被编译的,而 babel 现在应该是默认开启了对 ESModule 的支持。
代码分割
注*:此方法未经过验证。
目的就是为了分包和做到按需加载,有效的减少代码的大小。
这个项目其实没有什么特别好的展示方法,因为只有三个组件,并且存在彼此的依赖关系,不过这里还是会稍微带一下,可以之后再去看。
这里主要的核心概念是在 vue 的 router 组件中使用 webpack 提供的 require.ensure() 去做到按需加载。
require.ensure() 的语法如下:
require.ensure(
dependencies: String[],
callback: function(require),
errorCallback: function(error),
chunkName: String
)
大概方法如下:
const routes = [
{
path: '/',
name: 'index',
component: (resolve) =>
require.ensure([], () => resolve(require('./views'))),
},
{
path: '/otherComp',
name: 'other compoment',
component: (resolve) =>
require.ensure([], () => resolve(require('./views/otherComponent/'))),
},
// 差不多的用法
];
注*:除了 webpack 提供的 require.ensure()
之外其实还有其他的方法,不过这里主要还是注重 webpack 相关的学习,所以使用的是 webpack 提供的功能。
魔法注释
注*:此方法未经过验证。
这是让打包后的文件显示文件名的方法,如果文件名相同的将会被打包在一起。
大概方法如下:
const routes = [
{
path: '/',
name: 'index',
component: (resolve) =>
require.ensure([], () => resolve(require('./views'), 'index')),
},
{
path: '/otherComp',
name: 'other compoment',
component: (resolve) =>
require.ensure([], () =>
resolve(require('./views/otherComponent/'), 'otherComp')
),
},
// 差不多的用法
];
提取 CSS 文件
如果 CSS 的文件体积不是很大,那么直接将 CSS 嵌入到 modules 中说不定运行速度会更快一些——毕竟少了一次请求,还有请求头之类的数据传输。
当然,这里是做功能展示,所以就拆分了——这一步会将 CSS 提取到一个单独的文件中去,然后通过 link 的方式进行引用。
这里依旧会使用插件:mini-css-extract-plugin,去完成这个功能。
注*:这里没有对 .vue 文件内的行内样式进行处理,默认行内样式的尺寸不会特别大——至少不会大到去进行拆分的必要。
1、下载插件
npm install --save-dev mini-css-extract-plugin
2、使用插件
这里依旧会选择在生产环境下使用这个插件,具体的配置如下:
// 新的引用
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = merge(common, {
// 新增对CSS的处理,由原本的行内注释改为新增一个CSS文件
module: {
rules: [
{
test: /\.less$/i,
use: [
// style-loader 不需要了
// "style-loader",
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
],
},
],
},
plugins: [
// 新增插件
new MiniCssExtractPlugin(),
],
});
3、运行结果
依旧是使用 npm run build
去执行操作,最后能够发现输出的结果多了一个 css 文件:
看起来优化到这里可以结束了,但是当我打开 main.css 一看,这才发现,压缩没做:
回顾一下,webpack 可以直接对 JavaScript 进行处理,这也就意味着对 CSS 和 HTML 的处理需要依赖其他的 插件(plugins) 和 加载器(loaders)。
换言之,这里需要对 CSS 进行另外的处理。
压缩 CSS 文件
这里其实是有两个选项,分别对应两个版本:
-
webpack v4
optimize-css-assets-webpack-plugin
-
webpack v5
css-minimizer-webpack-plugin
这里的 webpack 的版本是 v5,所以会选择用 css-minimizer-webpack-plugin。
1、下载插件
npm install css-minimizer-webpack-plugin --save-dev
2、使用插件
这个插件的使用还……有点意思,配错了容易导致其他 optimization 优化失效,具体配置为:
// 新增引用
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = merge(common, {
// 其他配置不变
optimization: {
// 注意这个 '...'
// 它是必须的,并且只能用在 webpack v5 以上
// 不使用这个 '...' 会导致其他的优化失败
minimizer: ['...', new CssMinimizerPlugin()],
},
});
3、运行结果
可以看到,与只使用了 mini-css-extract-plugin 后的结果不一样,使用了 css-minimizer-webpack-plugin 后,css 已经被压缩了:
Hash 输出文件
这是为了客户端缓存进行了优化而使用的功能,一单文件名被 hash 了之后,那么在客户端就可以保存相对而言比较长的时间,而又不用担心更新的问题——新的文件名代表新的请求。
这个配置可以在这里找到:Avoid Production Specific Tooling,总共有三个选项:
- [fullhash]
这里的 fullhash 对应的应该就是 v4 的 hash,毕竟看起来结果是一样的:
fullhash 指的是每次 build 的时候,如果任何内容有所变动,都会重新生成一个。 hash 值,但是所有文件的 hash 是一样的
- [chunkhash]
以 chunk 为级别进行重新打包,这种情况下,每个 chunk 的 hash 值都是一样的。这个项目上看不出什么差别,不过如果使用了动态路由,那么每个 router 中引进的 chunk 都是不一样的,这时候就会生成不同的 hash 值。
同样,只有对应的 chunk 发生了变动,在重新打包的时候,因修改过的 chunk 而生成的文件名就会发生变动。
- [contenthash]
这个就是以文件为级别重新进行打包,如果文件产生了变动,那么对应的文件就会生成一个新的 hash 值。
如下图就能看出来,css 和 js 文件有两个不同的 hash 值:
webpack 官网讲的不是很细致,这里面的解释来自于: Webpack 4: hash and contenthash and chunkhash, when to use which?
修改过的代码在这里:
module.exports = merge(common, {
// 其余不变
output: {
// 其余不变
filename: '[name]-[contenthash:8].bundle.js',
},
plugins: [
// 其余不变
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css',
}),
],
});
注*:contenthash:8
中 :[num]
是用来指定生成既定长度的 hash 值,如 :8
就是 8 位长度。默认好像是 20
这次项目中使用的插件和加载器版本
就是从 package.json 当中拉出来的。
{
// 是一个 vue2 的项目
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11"
},
// 所有使用的开发依赖的版本
"devDependencies": {
"@babel/core": "^7.14.6",
"@vue/cli-plugin-babel": "^4.5.13",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.2",
"eslint": "^7.30.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-vue": "^7.12.1",
"eslint-webpack-plugin": "^2.5.4",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^10.0.0",
"mini-css-extract-plugin": "^2.0.0",
"style-loader": "^3.0.0",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.7",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.41.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"
},
// 这里的内容会被根目录下的 .eslintrc.js 所重写,所以像 extends 和 rules,还是需要参考上文的 .eslintrc.js 中的配置
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": ["plugin:vue/essential", "eslint:recommended"],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
}
}
最后的最后,不知道有没有人会看到这里……有需要项目完整打包上传的吗。