学一点Webpack配置:Webpack的优化
在上一篇中花了14小节主要和大家一起探讨了Webpack 4.x的一些基本配置,比如初始化项目,添加各种Loader、React、Typescript、Sass、PostCSS、CSS Modules等配置、图片加载、字体加载以及各种代码检测的能力,比如ESLint的配置、Prettier的配置和StyleLint的配置。在接下来这个部分,主要和大家一起来探讨Webpack 4.x中的一些优化方面的配置。比如开发环境的优化、Webpack自身的优化、文件压缩和依赖监控以及应用分析相关的配置。如果感兴趣的话,欢迎继续往下阅读。
在阅读接下来的内容之前,如果你没有阅读上一篇,建议你先花点时间阅读上一篇,这样会更有系统性的学习。
Step15:开发环境优化
请把分支切换到
step15
查看代码。
开启局部模块热重载
在这一节中我们来对基本环境做一些优化。在开发环境中引入webpack.HotModuleReplacementPlugin
用于启用局部模块热重载,方便我们开发。
我们在Step03中将Webpack的环境分离出来了,有关于Webpack开发环境的配置都将在webpack.dev.js
做调整。如果我们要在开发环境加上webpack.HotModuleReplacementPlugin
就需要在webpack.dev.js
的plugins
来做调整:
// ...
module.exports = merge(commonConfig, {
// ...
// 开发环境下需要的相关插件配置
plugins: [new webpack.HotModuleReplacementPlugin()],
// ...
});
另外在devServer
也做一些调整:
// 开发服务器
devServer: {
hot: true, // 热更新,无需手动刷新
contentBase: DIST_PATH, //
host: '0.0.0.0', // host地址
port: 8080, // 服务器端口
historyApiFallback: true, // 该选项的作用所用404都连接到index.html
overlay: {
// 当出现编译错误或警告时,就在页面上显示一层黑色的背景层和错误信息
errors: true,
},
inline: true,
},
使用静态资源路径
网页中总是会使用到一些静态资源,将这些静态资源部署到CDN服务上,使用得用户可以就近访问资源,加快访问速度。在Webpack中,我们可以通过publicPatch
来配置CDN服务器对应的URL
。
就我们这个工程而言,可以在webpack.common.js
中output
中指定publicPath
:
// webpack.common.js
// 编译输出的JS入路径
// 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
output: {
path: DIST_PATH, // 创建的bundle生成到哪里
filename: '[name].bundle.js', // 创建的bundle的名称
sourceMapFilename: '[name].js.map', // 创建的SourceMap的文件名
publicPath: '/', // 指定存放静态资源的CDN地址
},
为导出的JS文件添加hash
当我们修改代码,bundle
被重新打包,很可能在客户端上看到的效果并不是最新的效果,这有可能是缓存所造成的,有的时候需要多次刷新浏览器,甚至是要手动去清除缓存。不管是在开发环境还是生产环境,这直接影响我们的开发效率。在Webpack中我们可以很轻易的解决这个问题。只需要在输出的JS文件上加上hash
值,这样一来,每次输出的JS文件名都会不同,那么文件也就不会被缓存。
只需要在output
的filename
值上添加hash
值:
// webpack.common.js
output: {
path: DIST_PATH, // 创建的bundle生成到哪里
filename: '[name].bundle.[hash].js', // 创建的bundle的名称
sourceMapFilename: '[name].js.map', // 创建的SourceMap的文件名
publicPath: '/', // 指定存放静态资源的CDN地址
},
输出的bundle.js
会每次都带上相应的hash
值:
注意,如果你在Webpack配置中使用了webpack.HotModuleReplacementPlugin()
的话,那么在output.filename
中的值不能使用[chunkhash]
,只能使用[hash]
。
这样一来每次修改代码都会生成一个带hash
的JS文件。要是你执行npm run build
同样会这样,并且会让你项目中/dist
目录下有关于bundle.js
的文件会越来越多,比如:
编译前清理dist目录
上一节我们看到了,每次都会为bundle.js
新增文件,如果多次修改,再次编译或打包就会在/dist
目录下生成一堆的JS文件,但上一次打包的文件对于我们来说没有实际用处。因此,我们需要在每次打包时清除/dist
目录下的旧文件。
实现该功能,可以借助Webpack的CleanWebpackPlugin
来实现,在配置之前先安装该插件:
⇒ npm i clean-webpack-plugin -D
然后webpack.common.js
中添加相应的配置:
// webpack.common.js
// 注意最新版本引用CleanWebpackPlugin 不能使用 const CleanWebpackPlugin = require('clean-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ...
// 模块解析
module: {
rules: [
// ...
]
// 插件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
inject: true,
template: HtmlWebpackTemplate,
appMountId: 'root',
filename: 'index.html',
}),
],
};
另外一个细节,如果配置中同时出现CleanWebpackPlugin
和HtmlWebpackPlugin
时,CleanWebpackPlugin
需要放置在HtmlWebpackPlugin
的前面。
配置完成之后,你在命令终端执行npm run build
的时候,/dist
目录下中以前的bundle.js
会自动清除。
这里额外提一下,在HtmlWebpackPlugin
的配置中我们可以添加minify
相关的配置,这样会将打包生的.js
和.css
文件引入到.html
文件中:
// webpack.common.js
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
inject: true,
template: HtmlWebpackTemplate,
appMountId: 'root',
filename: 'index.html',
minify: {
removeComments: true, // 去掉注释
collapseWhitespace: true, // 去掉多余空白
removeAttributeQuotes: true, // 去掉一些属性的引号,例如id="moo" => id=moo
},
}),
],
有了上面的配置之后,在代码中的注释、空白等就会自动清除,比如App.tsx
中的注释:
<!-- App.tsx -->
<div style={{ textAlign: 'center' }}>
<img src={String(Security)} />
{/* 下面这样引用,图片会报404错误 */}
{/* <img src="../../../../assets/images/security.svg" alt=""/> */}
</div>
编译出来.html
中就看不到了。如下图所示:
Step16:Webpack的优化
请把分支切换到
step16
查看代码。
在这一步我们主要来一起探讨Webpack中的一些优化。
文件路径优化
在Webpack中我们配置了resolve.extension
之后可以不用在require
或import
的时候加文件扩展名,Webpack会依次尝试添加扩展名进行匹配。在resolve
中还可以通过alias
来配置别名,可以加快Webpack查找模块的速度。
// webpack.common.js
alias: {
'@': path.resolve(__dirname, '../src'),
'@components': path.resolve(__dirname, '../src/components'),
'@pages': path.resolve(__dirname, '../src/pages'),
'@images': path.resolve(__dirname, '../src/assets/images'),
'@fonts': path.resolve(__dirname, '../src/assets/fonts'),
'@icons': path.resolve(__dirname, '../src/assets/icons'),
},
在实际使用的时候,我们就可以使用定义好的别名了:
<!-- App.tsx -->
const Security = require('@images/security.svg');
// app.css
body {
background: url("~@images/body-background.jpg") no-repeat center,
linear-gradient(to bottom, #f560a9, #09aefa, #2390af) no-repeat center;
}
有一点需要特别注意,在TypeScript环境下,如果仅在Webpack中配置别名是不够的,比如:
<!-- App.tsx -->
import Button from '@components/Button/Button';
编译的时候会报错:
Failed to compile.
[at-loader] ./src/pages/index/components/App/App.tsx:5:20
TS2307: Cannot find module '@components/Button/Button'.
我们需要在tsconfig.json
中添加paths
的配置:
{
"compilerOptions": {
"sourceMap": true,
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"module": "es2015",
"target": "es6",
"lib": [
"es2015",
"es2017",
"dom"
],
"removeComments": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"moduleResolution": "node",
"pretty": true,
"jsx": "preserve",
"allowJs": true,
"baseUrl": ".",
"rootDir": "./src",
"outDir": "./dist/",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@pages/*": ["./src/pages/*"],
"@images/*": ["./src/assets/images/*"],
"@fonts/*": ["./src/assets/fonts/*"],
"@icons/*": ["./src/assets/icons/*"],
}
},
"include": ["./src/**/*"],
"exclude": ["node_modules"]
}
上面的问题就解决了。
优化CSS文件
如果不做相关的配置,打包的时候CSS代码会直接打到JavaScript文件中。在Webpack中使用MiniCssExtractPlugin
插件可以单独生成.css
文件。这样CSS和JavaScript文件可以并行下载,提高页面加载性能。
先安装mini-css-extract-plugin
:
⇒ npm i mini-css-extract-plugin -D
由于MiniCssExtractPlugin
插件还不支持**HMR
**,为了不影响开发效率,因为需要对开发环境做一下判断。目前有两种方式:
- 把
webpack.common.js
中有关于样式的配置分开到webpack.dev.js
和webpack.prod.js
中分别处理,在webpack.prod.js
中配置MiniCssExtractPlugin
插件 - 在
webpack.common.js
中通过process.env.NODE_ENV
的值来做判断,如果值为production
时才配置MiniCssExtractPlugin
插件
在这里我们采用第二种方式:
// webpack.common.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
// 模块解析
module: {
rules: [
// ...
// CSS Loader
{
test: /\.(sc|sa|c)ss$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../src'),
use: [
{
loader: process.env.NODE_ENV !== 'dev' ? MiniCssExtractPlugin.loader : 'style-loader',
options: {
sourceMap: true,
},
},
{
loader: 'css-modules-typescript-loader',
options: {
namedExport: true,
camelCase: true,
sass: true,
modules: true,
},
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]',
},
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
//...
],
},
// 插件
plugins: [
// ...
new MiniCssExtractPlugin({
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[name]-[id].[hash].css',
}),
],
};
执行npm run build
之后,CSS就抽取出来了,如下所示:
同时在/dist
目录下会新增一个/css
目录,抽出来的.css
文件也会放置到这个/css
目录下:
Webpack会把抽出来的CSS放置到/dist/index.html
的</head>
中:
虽然MiniCssExtractPlugin
成为CSS打包首选,相比之前可以异步加载、不重复编译,更易于使用,但其有一个缺陷,不支持CSS热更新。所以在开发环境之下,需要引入css-hot-loader
,以便支持热更新:
⇒ npm i css-hot-loader -D
在开发环境上加上css-hot-loader
:
// webpack.common.js
// CSS Loader
{
test: /\.(sc|sa|c)ss$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../src'),
use: [
{
loader:
process.env.NODE_ENV !== 'dev'
? MiniCssExtractPlugin.loader
: ['css-hot-loader', 'style-loader'],
options: {
sourceMap: true,
},
},
{
loader: 'css-modules-typescript-loader',
options: {
namedExport: true,
camelCase: true,
sass: true,
modules: true,
},
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]',
},
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
Webpack除了把CSS抽取出来之外,还有另两个插件可以帮助我们优化CSS:
optimize-css-assets-webpack-plugin
:把相同的样式合并css-split-webpack-plugin
:把过大的CSS文件拆分
下面我们来看看这两个插件如何配置到Webpack中。先安装这两个插件:
⇒ npm i optimize-css-assets-webpack-plugin css-split-webpack-plugin -D
注意这两个插件都是应用在生产环境下的,所以在webpack.prod.js
添加相应的配置:
// webpack.prod.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CSSSplitWebpackPlugin = require('css-split-webpack-plugin').default;
module.exports = merge(commonConfig, {
mode: 'production', // 设置Webpack的mode模式
devtool: 'cheap-module-source-map',
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true,
},
},
}),
],
},
// 生产环境下需要的相关插件配置
plugins: [
new CSSSplitWebpackPlugin({
size: 4000,
filename: '[name]-[part].[ext]',
}),
],
});
上面的代码中,我们看到了optimization
配置。该配置可以取替一些Webpack的插件,这里就不一一表述了。只提其中一个:
Webpack v4 中
optimization.splitChunks
和optimization.runtimeChunk
一起替代了CommonsChunkPlugin
插件,其中optimization.splitChunks
主要用于拆分代码;optimization.runtimeChunk
主要用于提取runtime
代码。
optimization
提供了一些默认配置:
optimization: {
minimize: env === 'production' ? true : false, // 开发环境不压缩
splitChunks: { // 拆分代码
chunks: 'async', // initial: 初始模块; async:按需加载模块; all: 全部模块
minSize: 30000, //模块超过3k自动被抽离成公共模块
minChunks: 1, // 模块被引用大于或等于1,便分割
maxAsyncRequests: 5, // 异步加载chunk的并发请求数量小于或等于5
maxInitialRequests: 3, // 一个入口并发加载的chunks数量小于或等于3
name: true, // 默认由模块名+hash命名,名称相同时多个模块将合并为一个,可以设置为function
automaticNameDelimiter: '~', // 命名分隔符
cacheGroups: { // 缓存组,会继承和覆盖splitChunks的配置
default: { // 模块缓存规则,设置为false,默认缓存组将禁用
minChunks: 2, //模块引用大于或等于2,拆分至vendors公共模块
priority: -20, // 优先级
reuseExistingChunk: true, // 默认使用已有的模块
},
vendors: {
test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
priority: -10
}
}
}
}
其中**optimization.splitChunks
是拆包优化的重点**, 如果你的项目引用了第三文件组件库,建议可以单独拆包,比如:
optimization: {
// ...
splitChunks: {
// ...
cacheGroups: {
antDesign: {
name: 'chunk-antDesign', //单独将AntD拆包
prioprity: 15, // 权重需大于其他缓存组
test: /[\/]node_modules[\/]ant-design[\/]/
}
}
}
}
有关于
SplitChunksPlugin
更详细的介绍可以点击这里查阅。
对于CSS方面的优化,其实还有CSS的压缩。这里暂时不做相关的介绍,会把图片压缩、JavaScript和CSS压缩放到一起来介绍。
浏览器缓存优化
在Step15步中为导出的JS文件添加hash
值,也是为了清除缓存的这个目的。这里我们借助HashedModuleIdsPlugin
来对其做一点优化。通过前面的学习,我们已经知道,使用HashedModuleIdsPlugin
可以当更改某个文件时,只改变对应文件的hash
值,而不是所有文件都会改变。那在前面的基础上,来调整一下Webpack相关的配置。
// webpack.common.js
output: {
path: DIST_PATH, // 创建的bundle生成到哪里
//filename: 'js/[name].bundle.[hash].js', // 创建的bundle的名称
//sourceMapFilename: 'js/[name].js.map', // 创建的SourceMap的文件名
//publicPath: '/', // 指定存放静态资源的CDN地址
},
在webpack.common.js
中output.filename
、output.sourceMapFilename
和output.publicPath
几个选项注释掉,然后针对开发环境和生产环境分进行不同的配置。
// webpack.dev.js
module.exports = merge(commonConfig, {
// ...
output: {
filename: 'js/[name].bundle.js', // 创建的bundle的名称
chunkFilename: 'js/[name].bundle.js',
sourceMapFilename: 'js/[name].bundle.js.map', // 创建的SourceMap的文件名
publicPath: '/', // 指定存放静态资源的CDN地址
},
// 开发环境下需要的相关插件配置
plugins: [
new webpack.NamedModulesPlugin(), //用于启动HMR时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(), // 开启模块热更新,热加载和模块热更新不同,热加载是整个页面刷新
],
// ...
}
// webpack.prod.js
output: {
filename: 'js/[name].[contenthash].js', // entry对应的key值
chunkFilename: 'js/[name].[contenthash].js', // 间接引用的文件会走这个配置
publicPath: '/', // 指定存放静态资源的CDN地址
},
优化Loader配置
在Webpack配置中会使用到各种Loader,比如babel-loader
、css-loader
、file-loader
等。我们可以对各种Loader做精准配置:
exclude
:指定“排除不处理的目录”include
: 精确指定“要处理的目录”
这样就会缩小Loader加载搜索的范围,高概率的命中文件。这样一来,就可以在webpack.common.js
的各种Loader配置的地方,加上范围的约束:
// webpack.common.js
rules: [
{
test: /\.(j|t)sx?$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
// ...
],
},
// CSS Loader
{
test: /\.(sc|sa|c)ss$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
// ...
],
},
// images loader
{
test: /\.(png|jp(e*g)|gif|svg|webp)(\?.*)?$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
// ...
],
},
// font loader
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
// ...
],
},
],
另外,Webpack4 的babel-loader
很慢,其实我们可以在babel-loader
后面显式的添加cacheDirectory
参数来改变之样的一个现状。
cacheDirectory
是Loader的一个特定的选项,其默认值是false
。我们可以显式指定目录用来缓存Loader的执行结果,这样就有可能减少Webpack构建时Babel重新编译过程。配置方式很简单,只需要在babel-loader
后加上cacheDirectory
即可:
loader: 'babel-loader?cacheDirectory',
// 或
loader: 'babel-loader?cacheDirectory=true',
resolve优化配置
Webpack的resolve.modules
配置模块库(即/node_modules
)所在的位置,在JavaScript里出现import * as React from "react"
这样的代码时,其寻找的模块路径不是相对的,也不是绝对的,代码会去/node_modules
目录下寻找。而默认的配置会采用向上递归搜索的方式去搜索。而一个项目中,通常只会有一个/node_modules
目录,而且都是在项目根目录下,为了减少搜索范围,我们在Webpack配置的时候可以直接写明/node_modules
的全路径。同样的原于,使用别名(alias
)的配置时,也可以这样做。
// webpack.common.js
// 将路径转换为绝对路径
function resolve(dir) {
return path.join(__dirname, dir);
}
在resolve
中就可以像下面这样写:
resolve: {
// 配置之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.css', '.scss'],
modules: [ // 优化模块查找路径
path.resolve('src'),
path.resolve('node_modules') // 指定node_modules所在位置 当你import 第三方模块时 直接从这个路径下搜索寻找
],
alias: {
'@': path.resolve(__dirname, '../src'),
'@components': path.resolve(__dirname, '../src/components'),
'@pages': path.resolve(__dirname, '../src/pages'),
'@images': path.resolve(__dirname, '../src/assets/images'),
'@fonts': path.resolve(__dirname, '../src/assets/fonts'),
'@icons': path.resolve(__dirname, '../src/assets/icons'),
},
}
配置好src
目录所在位置后,如果我们要引用在/src
下的资源就会变得更轻松一些。比如:
|--/src
|----/components
|------Button.js
|------Card.js
|----Appp.js
那么在App.js
中引入/components
下的资源可以像下面这样操作:
// App.js
import Button from 'component/Button';
import Card from 'component/Card'
DellPlugin提升打包速度
在开发的时候总是会用到第三方库,比如React库,这些库在很长一段时间内都有可能不会更新,打包的时候分开打包可以提升打包速度,DllPlugin
动态链接库插件,其原理就是把网页依赖的基础块抽离出来打包到dll
文件中,当需要导入的模块存在于某个dll
中时,这个模块不再被打包,而是去dll
中获取。
首先在/build
目录下创建一个webpack.dll.js
文件,并写入下面的配置:
const webpack = require('webpack');
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom'],
},
output: {
path: path.resolve(__dirname, '../static/dll'),
filename: '[name].dll.js',
library: '[name]_lib',
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../static/dll/**/*')],
}),
new webpack.DllPlugin({
path: path.resolve(__dirname, '../static/dll', '[name]-manifest.json'),
name: '[name]_lib',
context: process.cwd(),
}),
],
};
然后package.json
中scripts
添加新的执行命令:
"build:dll": "webpack --mode production --progress --config ./build/webpack.dll.js",
保存之后,在命令终端执行:
⇒ npm run build:dll
会在项目的根目录下创建/static/dll
目录,并且会生成vendors.dll.js
和vendors-manifest.json
文件:
因为第三方库不会轻易的更新,所以没有每次打包的时候都执行npm run build:dll
。另外,如果不想手动来将生成出来的/dll
目录下的.js
文件插入到index.html
的话,需要再安装一个依赖add-asset-html-webpack-plugin
,它会将我们打包后的*.dll.js
文件注入到生成的index.html
中。
先在命令端执行:
⇒ npm i add-asset-html-webpack-plugin -D
然后在webpack.prod.js
中添加相应的配置代码:
// ...
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');;
module.exports = merge(commonConfig, {
mode: 'production', // 设置Webpack的mode模式
devtool: 'cheap-module-source-map',
output: {
filename: 'js/[name].[contenthash].js', // entry对应的key值
chunkFilename: 'js/[name].[contenthash].js', // 间接引用的文件会走这个配置
publicPath: '/', // 指定存放静态资源的CDN地址
},
optimization: {
// ...
},
// 生产环境下需要的相关插件配置
plugins: [
// ...
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../static/dll/vendors.dll.js'), // 对应的 dll 文件路径
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '..', 'static/dll/vendors-manifest.json'),
}),
],
});
这个时候执行npm run build
就可以Build出打包后的文件。
**注意:**这里有一个细节需要注意,如果在执行
npm run build
之前需要至少先执行一次npm run build:dll
。
此时可以尝试执行http-server dist/
就可以访问打包好的页面。但会发现有一个报错信息:
修改一下webpack.dll.js
配置:
const webpack = require('webpack');
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom'],
},
output: {
path: path.resolve(__dirname, '../static/dll'),
filename: '[name].dll.js',
library: '[name]_lib',
libraryTarget: 'var',
pathinfo: true,
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../static/dll/**/*')],
}),
new webpack.DllPlugin({
path: path.resolve(__dirname, '../static/dll', '[name]-manifest.json'),
name: '[name]_lib',
context: path.resolve(__dirname),
}),
],
};
重新编译打包之后,再次访问报错信息就处理完了:
压缩文件
浏览器从服务器访问Web页面时会获取各种资源(HTML、JavaScript、CSS和图片等),如果文件越大,页面加载时间就会越长。为了提升页面加载速度和减少网络传输流量,可以对这些资源进行压缩。压缩的方法除了可以通过gzip
算法对文件压缩外,还可以对文件自身进行压缩。对文件进行压缩的作用除了有提升Web页面加载速度的优势外,还具有混淆源码的作用。
在Webpack的配置中,我们就可以做到对Web页面资源进行压缩。
注意,在开发阶段不需要考虑太多压缩这方面的事情,所以有关于压缩相关的配置都是
webpack.prod.js
文件下进行配置。
压缩JavaScript
目前压缩JavaScript常见的工具是UglifyJS和ParallelUglifyPlugin:
- UglifyJS:会分析JavaScript代码语法树,理解代码含义,从而能做到诸如去掉无效代码,去掉日志输出代码和缩短变量名等优化
- ParallelUglifyPlugin:会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行。所以能更快的完成对多个文件的压缩工作
一般情况,这两种插件选择其中一种即可。我们先来看UglifyJS插件在Webpack中的配置。
首先把分支切换到Step16-2,查看源码。
使用UglifyJS之前需要先安装相关的插件,可以在命令终端执行下面的命令:
⇒ npm i uglifyjs-webpack-plugin -D
然后在webpack.prod.js
中添加类似下面这样的配置代码:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(commonConfig, {
// ...
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
cache: true, // 开启文件缓存
parallel: true, // 使用多进程并行来提高构建速度
sourceMap: true, // 开启source map
warnings: false, // 在UglifyJS删除没有用到的代码时不输出警告
compress: {
drop_console: true, // 删除所有的console语句
collapse_vars: true, // 内嵌定义了但是只用到一次的变量
reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值
},
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
},
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true,
},
},
}),
],
},
// ...
});
保存上面的配置,执行npm run build
,你可以看到下面的相关反馈。
有关于UglifyJS可以查阅官网文档。
注意,在Webpack 4.x版本以前,
uglifyjs-webpack-plugin
还是放在plugins
中进行配置,在Webpack 4.x中新增了optimization
,上面的示例就是在该选项中进行配置的。
接下来再来看看ParallelUglifyPlugin
的配置。首先把分支切换到Step16-3
。
要使用该插件来压缩JavaScript代码,首先也需要安装该插件,所以在命令终端执行下面的命令:
⇒ npm i webpack-parallel-uglify-plugin -D
同样在webpack.prod.js
中optimization.minimizer
添加相应的配置:
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = merge(commonConfig, {
// ...
optimization: {
minimizer: [
new ParallelUglifyPlugin({
// 多进程压缩
uglifyJS: {
warnings: false,
output: {
comments: false,
beautify: false,
},
compress: {
drop_console: true,
collapse_vars: true,
reduce_vars: true,
},
},
}),
// ...
],
},
// 生产环境下需要的相关插件配置
// ...
});
保存文件,执行npm run build
的时候,打包代码就会对JavaScript代码进行压缩。
有关于
webpack-parallel-uglify-plugin
更详细的配置选项,可以查阅其官方文档。
压缩CSS
请把代码切换到Step16-4分支,查看源码。
CSS也可以像JavaScript那样进行压缩,以达到提升加载速度和代码混淆的作用。Webpack 4.x之后主要使用mini-css-extract-plugin
和optimize-css-assets-webpack-plugin
和cssnano
等插件:
mini-css-extract-plugin
:用来将CSS分离出来optimize-css-assets-webpack-plugin
:把相同的样式合并cssnano
:PostCSS插件,用来压缩CSS
在优化CSS一节中,我们其实介绍过了mini-css-extract-plugin
和optimize-css-assets-webpack-plugin
,并且在webpack.common.js
和webpack.prod.js
进行配置。事实上这两个插件帮我们优化了CSS代码,但并没有起到压缩CSS的作用。
时至今日,压缩CSS较好的插件是PostCSS插件cssnano
。前两个插件已经安装好了,现在只需要安装cssnano
:
⇒ npm i cssnano -D
然后在webpack.prod.js
中的new OptimizeCSSAssetsPlugin
添加有关于cssnano
的相关配置:
new OptimizeCSSAssetsPlugin({
cssProcessor: require('cssnano'),
cssProcessorOptions: {
discardComments: {
removeAll: true,
},
autoprefixer: false,
map: {
inline: false,
annotation: true,
},
},
canPrint: true,
}),
保存文件之后。一旦你执行npm run build
之后,打包的CSS就会压缩:
压缩HTML
请把分支切换到Step16-5分支,查看源代码。
在Step05中,我们安装了html-webpack-plugin
和html-webpack-template
两个插件:
html-webpack-plugin
:生成index.html
文件,并且自动插入到/dist
目录下html-webpack-template
:能够将元素id
附加到mount
另外安装了add-asset-html-webpack-plugin
插件让程序可以自动将生成出来的/dll
目录下的.js
文件插入到index.html
,并且会将打包后的*.dll.js
文件注入到生成的index.html
中。
有关于
add-asset-html-webpack-plugin
相关的配置可以查阅DellPlugin提升打包速度一节。
为了可以内联嵌入JavaScript和CSS源码,HTML Webpack Plugin还有另一个插件html-webpack-inline-code-plugin
可以帮助我们内联代码。
同样的,先安装该插件:
⇒ npm i html-webpack-inline-code-plugin -D
然后在webpack.common.js
中添加如下配置:
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
inject: true,
template: HtmlWebpackTemplate,
appMountId: 'root',
filename: 'index.html',
minify: {
removeComments: true, // 去掉注释
collapseWhitespace: true, // 去掉多余空白
removeAttributeQuotes: true, // 去掉一些属性的引号,例如id="moo" => id=moo
},
inlineSource: '.(js|css)$',
}),
new HtmlWebpackInlineSourcePlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name]-[id].[contenthash].chunk.css',
}),
],
保存文件,执行npm run build
打包文件之后,你看到的/dist/index.html
文件如下:
压缩图片
请把分支切换到Step16-6,查看源代码。
可以说,图片在Web中有可能占用的大半的资源。而资源过大直接会影响Web页面的加载和渲染。在Webpack中,如果资源过大的话,在打包的时候会有一些警告信息提示,比如我们示例中的/src/assets/images/body-background.jpg
,其大小大约是449 KB
:
大家都清楚,资源过大会占用很大一部分宽度。为了提升Web页面的性能,所以需要对图片进行优化。除了借助一些工具来优化图像之外,其实我们在Webpack中打包文件的时候就可以对图像做一些优化。在Webpack中可以使用url-loader
、svg-url-loader
和image-webpack-loader
来优化它们。
其实在Step11的时候,我们配置图片加载的时候就有介绍过url-loader
。该插件可以将小型静态文件内联到应用程序中。如果不进行配置,它将把接受一个传递的文件,将其放在已编译的包旁边,并返回该文件的url
。如果指定limit
选项,它将把小于这个限制的文件编码为Base64
数据的url
并返回这个url
,这将图像内联到JavaScript代码中,从而可以减少HTTP
的请求。
// webpack.common.js
// images loader
{
test: /\.(png|jp(e*g)|gif|svg|webp)(\?.*)?$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
{
loader: 'url-loader',
options: {
limit: 1024, // 小于10kb的图片编译成base64编码,大于的单独打包成图片
name: 'images/[hash]-[name].[ext]', // Placeholder占位符
publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
},
},
],
},
url-loader
插件可以在加载图片的时候就做一些优化,除此之外,使用image-webpack-loader
可以在打包的时候对JPG
、PNG
、GIF
、SVG
和WEBP
图像使用imagemin
进行压缩。
image-webpack-loader
插件不会把图片内嵌到应用程序,所以它必须与url-loader
(或file-loader
或后面要介绍的svg-url-loader
)一起使用。为了让该插件能正常使用,同样的需要先安装该插件:
⇒ npm i image-webpack-loader -D
然后在webpack.common.js
的module.rules
中url-loader
之后添加image-webpack-loader
相关的配置:
// images loader
{
test: /\.(png|jp(e*g)|gif|svg|webp)(\?.*)?$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
{
loader: 'url-loader',
options: {
limit: 1024, // 小于10kb的图片编译成base64编码,大于的单独打包成图片
name: 'images/[hash]-[name].[ext]', // Placeholder占位符
publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
},
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75,
},
},
},
],
},
保存之后执行npm run build
之后,发现.jpg
文件并没有得到任何的压缩:
在网上查了好久,似乎很多同学都碰到类似的问题,但并没有找到相应的解决方案,路过的大神求指点。
由于.jpg
并未得到压缩,为了验证配置是正确的(image-webpack-loader
能工作),我尝试着在页面中重新加载了一张.png
的图片:
.test__image {
background: url("~@images/test.png") no-repeat center;
background-size: cover;
width: 100vw;
height: 300px;
}
再次执行npm run build
之后,发现.png
图片得到了一定的压缩:
有关于
image-webpack-loader
更详细的配置,可以查看其官方文档。
对于.svg
文件的优化,除了url-loader
之外还可以使用svg-url-loader
,它将以utf-8
编码的DataUrl
字符串加载.svg
文件。其结果会比Base64更小,解析更快。
先安装该插件:
╰─➤ npm i svg-url-loader -D
然后在webpack.common.js
的module.rules
中添加相应的配置:
module: {
rules: [
// ...
// images loader
{
test: /\.(png|jp(e*g)|gif|webp)(\?.*)?$/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
{
loader: 'url-loader',
options: {
limit: 1024, // 小于10kb的图片编译成base64编码,大于的单独打包成图片
name: 'images/[hash]-[name].[ext]', // Placeholder占位符
publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
},
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75,
},
},
},
],
},
// svg loader
{
test: /\.svg/,
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
use: [
{
loader: 'svg-url-loader',
options: {
limit: 1024, // 小于10kb的图片编译成DataUrl编码,大于的单独打包成图片
name: 'images/[hash]-[name].[ext]', // Placeholder占位符
publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
noquotes: true,
},
},
],
},
],
},
执行npm run dev
之后,你会发现小于10kb
的.svg
文件编译之后和url-loader
不同之处:
有关于图片和SVG的优化,我们在后面还会花更多的时间来探讨。因为有关于这部分需要一起探讨和讨论的东西比较多,比如在使用SVG的时候,还可以考虑:
raw-loader
:允许访问原始SVG内容svg-inline-loader
:更进一步消除SVG中不必要的标记svg-sprite-loader
:可以将小的SVG合并成一个Sprite文件,从而更有效的加载,甚至配合其他的脚本,还可以将合并的Sprite代码直接内联到.index
文件中,比如《如何在Vue项目中使用SVG Icon》一文所讨论的。react-svg-loader
可以将SVG作为React组件使用
对于图片,除了上述介绍的之外,还可以使用resize-image-loader
和responsive-loader
生成srcset
属性所需要的图片资源,让现代浏览器可以根据环境选择最为合适的图片资源。
对于小图片,还可以使用webpack-spritesmith
将图像转为Sprite图和一些Sass、LESS或Stylus的混合宏。对于Sprtes图的生成和使用,还有其他类似的插件可供我们使用。
性能提示
如果想要在打包或者开发过程中展示一些性能提示,可以在 webpack.common.js
中加入如下配置:
module.exports = {
//...
performance: {
// 性能提示,可以提示过大文件
hints: 'warning', // 性能提示开关 false | "error" | "warning"
maxAssetSize: 100000, // 生成的文件最大限制 整数类型(以字节为单位)
maxEntrypointSize: 100000, // 引入的文件最大限制 整数类型(以字节为单位)
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return /\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetFilename);
},
},
// ...
}
Step17: 监控和分析应用程序
请把分支切换到Step17,查看源码。
不管是什么样的Web应用程序,随着不断的迭代开发,他加载的东西会越来越多,程序会越来越大。这个时候我们就需要配置一些监控和分析工作,让他们帮我们时时监控和分析所安装的依赖项。这让我们可以时刻保持对加载的依赖项有清晰的关注。
在Webpack配置中,我们可以使用以下几个插件来帮助我们做监控和分析:
webpack-dashboard
:通过展示依赖项大小、进度和其他细节来增强 Webpack 输出,有助于跟踪大型依赖项bundlesize
:用于验证 Webpack 的资源不超过指定的大小,当应用程序变得太大时能够及时得知webpack-bundle-analyzer
:能够扫描bundle
并对其内部内容进行可视化呈现,从而可以发现大型的或者不必要的依赖项
先在命令终端执行下面的命令,来安装这三个插件:
╰─➤ npm i webpack-dashboard bundlesize webpack-bundle-analyzer -D
先来看webpack-dashboard
相关的配置,在webpack.common.js
中引入该插件,并在plugins
中调用该插件:
const Dashboard = require('webpack-dashboard'); // 引入dashboard
const DashboardPlugin = require('webpack-dashboard/plugin'); // 引入dashboard对应的插件
const dashboard = new Dashboard(); // 创建一个dashboard的实例
然后在plugins
中添加DashboardPlugin
:
plugins: [
// ...
new DashboardPlugin(dashboard.setData),
],
然后在package.json
中的--quiet
:
"scripts": {
// ...
"dev": "webpack-dev-server --mode development --quiet --inline --progress --open --config ./build/webpack.dev.js",
//...
},
这样,在命令终端执行npm run start
启动服务时,就可以看到类似下图这样的结果:
再来配置bundlesize
。根据其官方文档所描述,我们可以在package.json
或者单独创建配置文件bundlesize.config.json
来指定具体的值。比如:
// package.json
{
"bundlesize": [
{
"path": "./dist/assets/images/*.png",
"maxSize": "50 kB",
},
{
"path": "./dist/assets/images/*.jpg",
"maxSize": "50 kB",
},
{
"path": "./dist/assets/images/*.svg",
"maxSize": "50 kB",
},
{
"path": "./dist/css/*.css",
"maxSize": "20 kB",
},
{
"path": "./dist/js/*.js",
"maxSize": "20 kB",
},
{
"path": "./dist/vendors.*.js",
"maxSize": "35 kB",
}
]
}
我这里选择在项目根目录下单独创建一个配置文件bundlesize.config.json
,并且添加上面示例代码的配置。
{
"files": [
{
"path": "./dist/assets/images/*.png",
"maxSize": "50 kB",
},
{
"path": "./dist/assets/images/*.jpg",
"maxSize": "50 kB",
},
{
"path": "./dist/assets/images/*.svg",
"maxSize": "50 kB",
},
{
"path": "./dist/css/*.css",
"maxSize": "20 kB",
},
{
"path": "./dist/js/*.js",
"maxSize": "20 kB",
},
{
"path": "./dist/vendors.*.js",
"maxSize": "35 kB",
}
]
}
保存文件之后在package.json
的scripts
中添加下面这样的配置:
"scripts": {
// ...
"check-size": "bundlesize --config ./bundlesize.config.json",
// ...
},
在命令终端执行npm run check-size
之后,你可以看到类似下图这样的结果:
就该示例配置来说,建议先执行
npm run build
,然后再执行npm run check-size
。如果会有报错信息,根据相关的提示信息做相应调整,然后再重新执行这两个命令。
接着再来配置webpack-bundle-analyzer
,同样的在webpack.common.js
中引入该插件:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
然后在module.exports.plugins
中调用BundleAnalyzerPlugin
:
module.exports = {
// ...
plugins: [
// ...
new BundleAnalyzerPlugin(),
],
};
保存webpack.common.js
文件之后,在命令终端执行npm run start
,浏览器会重启一个页面。该统计页面显示的是已解析文件的大小(当文件出现在包中时)。您可能想比较 gzip
之后的大小,因为它更接近实际用户体验,可以使用左边的边栏来切换大小。
对于报告,我们需要关注的点有:
- 大型依赖项:为什么这么大?是否有更小的替代方案?您是否使用了该库包含的所有代码?总之寻找问题所在,然后找到相应替代方案
- 重复的依赖关系:您是否看到同一个库在多个文件中重复出现?在Webpack4.x中使用
optimization.splitChunks.chunks
将重复的依赖关系移动到一个公共文件。或者某个包具有相同库的多个版本? - 相似的依赖关系:是否有类似的库可以做大致相同的工作?
有关于Webpack中配置依赖关系的监控和应用分析,更详细的介绍可以阅读@Ivan Akulov的《Monitor and analyze the app》一文。
小结
有关于Webpack配置还有很多知识可以探讨。在接下来我将还会继续和大家一起探讨这方面的知识。比如多页面的配置,脚手架的配置等。如果您对这方面知识感兴趣的话,欢迎持续关注后续的更新,如果您在这方面有相关的经验或更好的建议,欢迎在下面的评论中与我们一起共享。