三 webpack 生产环境的基本配置
上一篇文章我们讲完了 webpack 开发环境的配置,接下来就该讲 webpack 生产环境的配置了。前面已经说到,开发环境是启用代码本地调试运行的环境,帮助我们更高效率的开发。而生产环境则需要启动能让代码优化上线运行的环境,我们需要进行一系列的优化操作,以达到更好的用户体验。
3.1 将CSS抽离成单独的文件
之前我们处理 css 资源,最终都是使用 style-loader 在 js 文件中去动态创建 style 标签,再把样式写入 style 标签里。这样做有一个问题在于,会使打包后的 index.js 文件体积非常大,并且在读取 js 代码的过程中因为 js 单线程的原因造成样式渲染速度较慢,导致页面出现短暂的白屏闪烁。因此,我们需要替代之前将 css 写入 js 文件的做法,将 css 文件单独抽离出来,再通过 link 标签引入。同时为了减少网络请求的带宽浪费,我们需要将抽离出来的多个 css 文件合并为一个单独的 css 文件。实现这个功能,我们需要借助一个名为 mini-css-extract-plugin 的插件:
npm i mini-css-extract-plugin --save-dev
引入过后,我们首先要在 plugins 中对其进行实例化,然后在处理 css 资源的 loader 中用该插件的 loader 属性去替换原来的 style-loader:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 将 css 抽离成单独的文件
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 打包样式资源
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // 替换原来的 style-loader
'css-loader',
'less-loader'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 替换原来的 style-loader
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin() // 实例化 mini-css-extract-plugin 插件
],
mode: 'development'
}
我们在 src 目录下创建两个 css 文件并在 index.js 中引入,然后执行 webpack:
你会发现打包后的 dist 目录多了一个 main.css 文件,并将 a.css 和 b.css 文件的样式都包含进去,而不是像之前直接写入打包后的 built.js 文件了:
main.css 是默认命名,如果你想修改命名,可以在刚才实例化插件的地方传入对应的参数:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 将 css 抽离成单独的文件
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 打包样式资源
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/index.css' // 对抽离的css文件进行重命名
})
],
mode: 'development'
}
3.2 对CSS进行兼容性处理
css 里有很多样式在不同的浏览器里需要使用不同的写法,比如像弹性盒子 flex。但是,如果每次在写 css 的过程要自己去针对不同浏览器写不同的样式,那工作量将是难以想象的。好在已经有一些现成的工具,来帮助我们完成这些工作。对 css 进行兼容性处理,我们需要同时安装 postcss-loader 与 postcss-preset-env 插件(新版本的 postcss-loader 用法发生很大的变化,笔者暂时还没有弄懂,只能暂时用 3.0.0 版本了,以下过程基于该版本才能正常运行):
npm i postcss-loader@3.0.0 postcss-preset-env --save-dev
loader 的写法,除了用字符串的形式,还可以用对象的形式,如:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader' // 对象写法,等同于直接写 'css-loader'
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/index.css' // 对抽离的css文件进行重命名
})
],
mode: 'development'
}
loader 直接写字符串的方式,会使用该 loader 默认的配置;而使用对象的形式,我们可以在 options 属性里进行一些自定义配置。对于 3.0.0 版本的 postcss-loader,我们必须使用对象的方式,配合 postcss-preset-env 插件(该插件提供预设的 css 兼容代码),才可以正常的运行:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 打包样式资源
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader', // 对 css 进行兼容性处理
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')() // 使用 postcss-preset-env 插件读取 node 环境变量
]
}
}
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')()
]
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/index.css' // 对抽离的css文件进行重命名
})
],
mode: 'development'
}
尝试运行这个文件,你会看到打包后的 css 文件,确实对某些样式做了兼容性处理,但对弹性盒子并没有做兼容性处理。原因在于该 loader 默认使用的是开发环境的环境变量,只兼容部分新版本浏览器,并未对所有版本浏览器进行兼容。对此,我们可以手动在 package.json 文件中,在 browserslist 属性下对开发环境和生产环境进行不同程度的浏览器清单兼容配置:
// package.json 文件(JSON文件不支持包含注释,实际使用请去掉注释)
{
"name": "webpack-study",
"version": "1.0.0",
"description": "",
"main": "index2.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server"
},
"author": "可乐",
"license": "ISC",
"devDependencies": {
"css-loader": "^5.0.1",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"less": "^4.0.0",
"less-loader": "^7.2.1",
"mini-css-extract-plugin": "^1.3.4",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.44.2",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.2"
},
// 浏览器清单兼容配置
"browserslist": {
// 开发环境配置
"development": [
"last 1 chrome version", // 只兼容到上一个版本的谷歌浏览器
"last 1 firefox version", // 只兼容到上一个版本的火狐浏览器
"last 1 safari version" // 只兼容到上一个版本的safari浏览器
],
// 生产环境配置
"production": [
">0.1%", // 只对市场份额大于 0.1% 浏览器进行兼容
"not dead", // 不兼容已死掉的浏览器
"not op_mini all" // 不兼容所有 Opera Mini 浏览器(该浏览器依旧有使用群体,可根据实际情况配置)
]
}
}
接着我们要在 webpack.config.js 文件中利用 process.env.NODE_ENV (node 中的 process 对象类似于浏览器中的 window 对象)修改 node 当前的环境变量:(注意:postcss-preset-env 插件读取的不是当前配置的 webpack 的 mode,而是 node 的环境变量)
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 设置 node 环境变量 ( development 或 production )
process.env.NODE_ENV = 'production';
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 打包样式资源
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader', // 对 css 进行兼容性处理
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')() // 使用 postcss-preset-env 插件根据当前 node 环境变量对 css 按指定条件进行处理
]
}
}
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')()
]
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/index.css' // 对抽离的css文件进行重命名
})
],
mode: 'development'
}
接着运行 webpack,你会发现弹性盒子也被做了兼容性处理,如果你所设置的市场份额为 >0%,它会兼容所有版本的浏览器,对 border-radius 这样的样式都会做兼容性处理:
对于生产环境的浏览器兼容配置,官方推荐的写法是这样:
{
"name": "webpack-study",
"version": "1.0.0",
"description": "",
"main": "index2.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server"
},
"author": "可乐",
"license": "ISC",
"devDependencies": {
"css-loader": "^5.0.1",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"less": "^4.0.0",
"less-loader": "^7.2.1",
"mini-css-extract-plugin": "^1.3.4",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.44.2",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.2"
},
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
"last 1 version",
"> 1%",
"maintained node versions",
"not dead"
]
}
}
想要了解更多的 browserslist 配置,可阅读该文档 https://github.com/browserslist/browserslist#readme 。
3.3 压缩CSS文件
生产环境对 css 进行的最后一步处理,就是进行文件压缩。在 webpack 中对 css 的压缩处理非常简单,我们只需要引入 optimize-css-assets-webpack-plugin 插件,然后在 plugins 中实例化一下就好了。这里就不多说了,直接上代码:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') // 压缩 css
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 打包样式资源
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader', // 对 css 进行兼容性处理
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')() // 读取 postcss-preset-env 插件的环境变量
]
}
}
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')()
]
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/index.css' // 对抽离的css文件进行重命名
}),
// 压缩css
new OptimizeCssAssetsWebpackPlugin()
],
mode: 'development'
}
3.4 对JS进行兼容性处理
现在的主流浏览器基本支持新一代 ES6 语法,不过老版本的浏览器很多都不能解析 ES6 语法,这种时候我们就需要对 JS 进行兼容性处理,将 ES6 语法编译成老版本浏览器可以识别的 ES5 甚至 ES3 语法。对 JS 进行兼容性处理,我们需要使用到 babel-loader,这个 loader 依赖于一个核心库 @babel/core,所以我们需要同时下载这个库:
npm i babel-loader @babel/core --save-dev
对JS代码进行兼容分三种情况:
第一种情况是只做基本的兼容,它可以将 let、const 之类的关键字,转换为老版本浏览器可以识别的 var 关键字,我们需要安装一个 @babel/preset-env 的依赖包,指示 babel 以该规则进行兼容:
npm i @babel/preset-env --save-dev
接着我们修改 webpack.config.js 文件如下:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 对js进行兼容性处理
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 指示babel按 preset-env 的规则进行基本的兼容处理
presets: [
[
'@babel/preset-env'
]
]
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
mode: 'development'
};
现在我们写一些 ES6 语法的代码,然后运行 webpack:
我们可以看到打包出来的代码,成功将箭头函数转换成了普通函数,但是它无法处理全新的 ES6 机制的东西,如 Promise 对象。
第二种兼容JS的情况,是进行完整的JS兼容,对所有主流浏览器的版本以及所有全新的ES6语法进行兼容。要实现这种全盘兼容,我们需要安装一个名为 @babel/polyfill 的库:
npm i @babel/polyfill --save-dev
安装完成后,我们要应用该库,并不需要在 webpack.config.js 文件进行配置,只要在入口文件头部引入它:
然后运行 webpack,你会发现打包后的 js 文件体积变得非常的巨大,从原来的 4kb 直接变成了 504kb,打包出来的文件也多了很多兼容代码:
我们并不总是希望代码体量如此庞大,这其中会产生很多没有必要的代码。如果我们希望根据自己的项目需求,对指定的浏览器版本进行兼容,这时就需要安装一个叫 core-js 的库了:
npm i core-js --save-dev
这个库包含了对所有浏览器的兼容性代码。但有一个好处在于,它支持按需引入。安装完成后,我们需要在 webpack.config.js 中进行如下配置:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 对js进行兼容性处理
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
// 指定兼容方式, 配置为'usage'实现按需加载
useBuiltIns: 'usage',
// 指定core-js版本
corejs: {
version: 3
},
// 指定兼容某个版本之后的浏览器
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
mode: 'development'
};
重新运行一下 webpack,你会看到打包后的代码体积直接缩小到119kb了。如果你没有配置 targets 属性,则该插件会根据 package.json 里的 browserslist 属性进行兼容。想要对这些相关属性进行更多的了解,推荐阅读 Babel 7 升级实践 。
3.5 压缩JS文件
css 需要压缩,js 就更不用说了。在 webpack4+ 后的版本里,压缩 js 的写法变得非常的容易了。我们只需要修改 mode 为生产环境即可:
const { resolve } = require('path');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'dist')
},
// webpack4+ 将 mode 设为 production 即可压缩 js 文件
mode: 'production'
};
配置为生产模式,你就可以看到打包后的 built.js 被压缩了:
3.6 压缩HTML文件
压缩 html 也很简单,我们只需在原来的 html-webpack-plugin 插件的实例中设置 minify 属性即可:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
// 压缩 html文件
minify: true
})
],
mode: 'production'
};
该属性还允许进行自定义配置,如:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
// 压缩 html文件
minify: {
collapseWhitespace: true, // 移除空格
removeComments: true // 移除注释
}
})
],
mode: 'production'
};
3.7 生产环境配置总结
最后,我们前面描述的这些配置,结合一下开发环境的配置进行总结,就能得到生产环境的基本总配置了:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
// 设置 node 环境变量
process.env.NODE_ENV = 'production';
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
// 单独抽离css文件并进行兼容性处理
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // 单独抽离css文件
'css-loader',
'less-loader',
{
loader: 'postcss-loader', // 对 css 进行兼容性处理(需要配置 package.json 文件的 browserslist 属性)
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')() // 读取 postcss-preset-env 插件的环境变量
]
}
}
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 单独抽离css文件
'css-loader',
{
loader: 'postcss-loader', // 对 css 进行兼容性处理(需要配置 package.json 文件的 browserslist 属性)
options: {
ident: 'postcss',
plugins: () => [
require('postcss-preset-env')() // 读取 postcss-preset-env 插件的环境变量
]
}
}
]
},
// 对js进行兼容性处理
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 预设:指示babel做怎么样的兼容性处理
presets: [
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
// 指定core-js版本
corejs: {
version: 3
},
// 指定兼容性做到哪个版本浏览器
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
},
{
test: /\.(jpg|png|gif)/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
outputPath: 'imgs',
esModule: false
}
},
{
test: /\.html$/,
loader: 'html-loader' // 处理 html文件 src 属性引入的图片资源
},
{
exclude: /\.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
},
plugins: [
// 压缩html文件
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
collapseWhitespace: true, // 移除空格
removeComments: true // 移除注释
}
}),
new MiniCssExtractPlugin({
filename: 'css/index.css' // 对抽离的css文件进行重命名
}),
// 压缩css
new OptimizeCssAssetsWebpackPlugin(),
// 配置JS检查规则
new ESLintPlugin({
fix: true, // 对打包生成的文件自动纠错
extensions: ['js', 'json'], // 只检查 js、json 结尾的文件
exclude: '/node_modules/' // 排除 node_modules
})
],
// 生产环境默认压缩js文件
mode: 'production'
}
// package.json 文件 (实际使用时请把注释语句全部去掉)
{
"name": "webpack-study",
"version": "1.0.0",
"description": "",
"main": "index2.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server"
},
"author": "可乐",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.12.13",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.12.13",
"babel-loader": "^8.2.2",
"core-js": "^3.8.3",
"css-loader": "^5.0.1",
"eslint": "^7.19.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.22.1",
"eslint-webpack-plugin": "^2.4.3",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"less": "^4.0.0",
"less-loader": "^7.2.1",
"mini-css-extract-plugin": "^1.3.4",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.44.2",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.2"
},
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
"last 1 version",
"> 1%",
"maintained node versions",
"not dead"
]
},
"eslintConfig": {
"extends": "airbnb-base",
"rules": {
"no-console": "off"
},
"env": {
"browser": true
}
}
}