前言
【基础篇】还不敢动 webpack 的配置 ?的续文,在基础上增加如 PostCss 插件的运用、构建速度的提升、打包文件体积的优化、集成Eslint等。
如果不熟悉 webpack 基本的概念,可以先看基础篇。
实践
PostCSS
是一个用 JavaScript 工具和插件转换 CSS 代码的工具
先来讲一下最常见的如autoprefixer
插件,用于自动补齐 CSS3 前缀,从而保证 CSS3 样式兼容各大浏览器。
安装 npm i postcss-loader autoprefixer -D
。
在webpack.prod.js
添加如下代码。
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
+ {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ plugins: ['autoprefixer']
+ }
+ }
+ }
+ ]
},
早期配置browsers
需在 autoprefixer 的 options 进行配置,现官方推荐在package.json
文件以browserslist
为 key 值去配置或者新建.browserslistrc
文件进行配置。
browserslist 官网🐱🏍
本文以package.json
增加字段为例,如下所示。
+ browserslist: [
+ "last 2 Chrome versions",
+ "> 0.2%",
+ "ios 7"
+ ],
证明一下配置是否成功可以尝试用 transform 属性再进行打包,可发现打包后的文件自动添加了前缀。
还有常用的如postcss-pxtorem
和postcss-px-to-viewport
,这我之前有一篇【自适应】px 转 rem,你还在手算么?文章通过新建postcss.config.js
文件去完成 px 转 rem 的。那今天就教大家完成 px 转 vw 的配置。
和autoprefixer
一样,在 plugins 添加,如下所示。
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
+ [
+ 'postcss-px-to-viewport',
+ {
+ viewportWidth: 1920, // 设计稿定的基准宽
+ unitPrecision: 5, // 转换后保留的小数位数
+ minPixelValue: 1 // 小于或等于 1px 不转换 vw
+ }
+ ]
]
}
}
}
]
}
重新打包可观测到已经转为 vw 的单位。
多页面打包
多页面打包主要是配置 entry 字段以及 HtmlWebpackPlugin,多应用于 MPA(多页面应用)。当页面数量过多,需要设置多个入口及对应配置页面太耗费时间,所以使用 glob 动态获取入口文件。
代码摘自《极客时间-玩转Webpack》程柳峰大佬👍。
安装 npm i glob -D
编写函数如下,即插即用,根据项目结构更改正则即可,官网👉html-webpack-plugin。
const glob = require('glob')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
Object.keys(entryFiles)
.map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
})
);
});
return {
entry,
htmlWebpackPlugins
}
}
静态资源内联
资源内联(inline resource),是将一个资源以内联的方式嵌入另一个资源里面。
主要目的是监控上报相关打点、减少维护成本、提高页面加载性能及交互体验。
- HTML 内联使用
raw-loader@0.5.1
,多应用在多页共用相同的 meta 头,安装后在模板index.html
引入即可。
<!DOCTYPE html>
<html lang="en">
<head>
<%= require('raw-loader!./meta.html') %>
</head>
<body>
<div id="app"></div>
</body>
</html>
- CSS 内联在基础篇也有讲过用
mini-css-extract-plugin
将产生的所有 CSS 提取成一个独立的文件,以 link 方式引入。若想以 style 标签包裹嵌入 head 标签需要添加html-inline-css-webpack-plugin
来实现 CSS 内联的功能,。
+ const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
}),
new HtmlWebpackPlugin({
template: './src/index.html',
inject: true,
filename: 'index.html'
}),
+ new HTMLInlineCSSWebpackPlugin()
]
- JS 内联也可使用
raw-loader@0.5.1
,如引用lib-flexible
, index.html 添加行。
<!DOCTYPE html>
<html lang="en">
<head>
<%= require('raw-loader!./meta.html') %>
+ <script>
+ <%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %>
+ </script>
</head>
<body>
<div id="app"></div>
</body>
</html>
- 图片、字体内联借助
url-loader
,当图片或字体文件大小小于 10k 可转base64。webpack5已内置,可使用asset/inline
替代。
基础库分离
使用 webpack 提供的 externals 的配置,防止把某些 import
的包打包到最终的bundle
中,而是在运行中再去外部获取这些扩展依赖,优化压缩打包后文件大小以提高页面响应速度。
以 Vue 项目为例,可以将诸如 vue、vue-router、vuex 等以 cdn 的方式引入。以 vue 为例,在 webpack.prod.js 增加如下代码。
module.exports = {
+ externals: {
+ vue: 'Vue'
+ }
}
使用官方推荐的 unpkg 或 jsDelivr 的 CDN 网站复制链接,在 index.html 添加如下代码。
<!DOCTYPE html>
<html lang="en">
<head>
<%= require('raw-loader!./meta.html') %>
<script defer="defer">
<%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %>
</script>
+ <script defer="defer" src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
打包后可发现 main.js 如今只有7kb
。
除这种方式之外,webpack4 后内置的SplitChunksPlugin
插件也可将公共代码分离出来。
看下SplitChunk
的默认配置
optimization: {
splitChunks: {
// 哪些 chunks 进行分割,可选值:async、initial、all
chunks: 'async',
// 分离的 chunk 必须大于等于 minSize,默认20000, 约20kb
minSize: 20000,
// 通过拆分后剩余的最小 chunk 体积不能小于 0 。development 模式中默认为 0,其他情况,默认为 minSize 的值
minRemainingSize: 0,
// 分离的 chunk 至少被引用 1次
minChunks: 1,
// 按需加载文件,并行请求的最大数目
maxAsyncRequests: 30,
// 加载入口文件,并行请求的最大数目
maxInitialRequests: 30,
// 执行拆分的大小阈值,其他限制(minRemainingSize、maxAsyncRequests、maxInitialRequests)将被忽略
enforceSizeThreshold: 50000,
// cacheGroups 可配置多个组,每个组根据 test 设置条件,符合 test 条件的模块,就分配到该组。模块可被多个组引用,最终根据 priority 决定打包到哪个组。默认将所有 node_modules 目录打包至 vendors 组,将两个以上的 chunk 所共享的模块打包至 default 组
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
// 缓存组打包的先后优先级
priority: -10,
// 设置是否重用当前 chunk
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
若项目本身,更改 chunks 为 'all',即可将我们的基础库 vue 等进行一个分离,且可单独以 name 字段命名每一个 chunk。
Eslint
Eslint 目的是统一的团队代码格式,提高代码的可读性、可维护性。
步骤如下,最终会生成一份 .eslintrc.js
文件。
# 安装 eslint
npm i eslint -D
# 初始化配置文件,按提示操作即可
npx eslint --init
优化命令行的构建日志
通过 stats 字段更精确地控制 bundle 信息怎么显示,常见有如下几种选项。
预设 | 描述 |
---|---|
errors-only | 只在发生错误时输出 |
errors-warnings | 只在发生错误或新的编译时输出 |
minimal | 只在发生错误或新的编译开始时输出 |
none | 没有输出 |
normal | 标准输出 |
verbose | 全部输出 |
可在开发和生产环境中将 stats 字段配置成 errors-only,仅显示错误信息。
在设置 stats 字段的基础上使用 friendly-errors-webpack-plugin 美化输出信息。
安装 npm i friendly-errors-webpack-plugin -D
,plugins 增加配置。
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
}),
new HtmlWebpackPlugin({
template: './src/index.html',
inject: true,
filename: 'index.html'
}),
new HTMLInlineCSSWebpackPlugin(),
+ new FriendlyErrorsWebpackPlugin()
]
配置完成,可打包进行测试。
抽离公共配置
目前,开发和生产环境存在着重复的代码,所以,需要抽离公共配置。为了将这些配置合并,需要 webpack-merge 工具。
安装 npm i webpack-merge -D
,并新建 webpack.common.js 文件。
直接放抽离结果 ~
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const fileRoot = process.cwd()
module.exports = {
entry: './src/main.js',
output: {
path: path.join(fileRoot, 'dist'),
filename: '[name]_[chunkhash:8].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
[
'postcss-px-to-viewport',
{
viewportWidth: 1920,
unitPrecision: 5,
minPixelValue: 1
}
]
]
}
}
}
]
},
{
test: /.(png|jpg|gif|jpeg)$/,
type: 'asset/resource',
generator: {
filename: '[name][hash:8].[ext]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: '[name][hash:8].[ext]'
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
}),
new HTMLInlineCSSWebpackPlugin(),
new FriendlyErrorsWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
inject: true,
filename: 'index.html'
})
],
stats: 'errors-only'
}
webpack.dev.js 简化如下。
const webpackMerge = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const developmentConfiguration = {
mode: 'development',
devServer: {
port: 3000,
hot: true,
open: true
}
}
module.exports = webpackMerge.merge(commonConfiguration, developmentConfiguration)
webpack.prod.js 简化如下,基础库的分离二选一。
const webpackMerge = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const productionConfiguration = {
mode: 'production',
plugins: [],
optimization: {
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()]
// splitChunks: {
// chunks: 'all',
// cacheGroups: {
// vendors: {
// test: /[\\/]node_modules[\\/]/,
// // 缓存组打包的先后优先级
// priority: -10,
// // 设置是否重用当前 chunk
// reuseExistingChunk: true,
// name: 'vendor'
// },
// default: {
// minChunks: 2,
// priority: -20,
// reuseExistingChunk: true
// }
// }
// }
},
externals: {
vue: 'Vue'
}
}
module.exports = webpackMerge.merge(commonConfiguration, productionConfiguration)
速度分析
安装 npm i speed-measure-webpack-plugin -D
。
在 webpack.prod.js 文件的 plugins 的配置中加入插件。
const webpackMerge = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
+ const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')
const productionConfiguration = {
mode: 'production',
plugins: [
+ new SpeedMeasureWebpackPlugin()
],
optimization: {
minimizer: [new CssMinimizerPlugin()]
// splitChunks: {
// chunks: 'all',
// cacheGroups: {
// vendors: {
// test: /[\\/]node_modules[\\/]/,
// // 缓存组打包的先后优先级
// priority: -10,
// // 设置是否重用当前 chunk
// reuseExistingChunk: true,
// name: 'vendor'
// },
// default: {
// minChunks: 2,
// priority: -20,
// reuseExistingChunk: true
// }
// }
// }
},
externals: {
vue: 'Vue'
}
}
module.exports = webpackMerge.merge(commonConfiguration, productionConfiguration)
打包完成后,可输出各个 loader、plugin 的解析时间,如下所示。
体积分析
安装 npm i webpack-bundle-analyzer -D
,帮助分析项目依赖第三方模块和业务代码的大小。
和速度分析插件配置一致,在 webpack.prod.js 的 plugins 字段引入即可。
+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
plugins: [
new SpeedMeasureWebpackPlugin(),
+ new BundleAnalyzerPlugin()
]
根据生成的矩阵树图做针对性优化。
多线程并行构建
当项目变的臃肿后,需要采取多进程打包去优化构建。若项目体积不大,则无需开启多进程打包,因为开启进程同样需要花费时间。
安装 npm i thread loader -D
。至于 happypack 插件目前已不再维护,作者本身也是推荐使用thread loader
。
在 webpack.common.js 中针对耗时较长的文件添加 thread-loader,如解析 js 。
注意: 放置在其他 loader 之前,放置在 thread-loader 后的 loader 会在一个单独的 worker 池中。
rules: [
{
test: /\.js$/,
use: [
+ {
+ loader: 'thread-loader',
+ options: {
+ worker: 2
+ }
+ },
{
loader: 'babel-loader'
}
]
}
]
多线程并行压缩
在基础篇有谈到关于 JS 压缩使用 TerserPlugin。
并行需要将 parallel 字段设置为 true,同样只适用于项目 JS 文件过大的情况。
optimization: {
minimizer: [
new TerserPlugin({
+ parallel: true
}),
new CssMinimizerPlugin()
]
}
DllPlugin
DllPlugin 作用是预编译资源模块,意思是提前将依赖库进行编译打包,生成一个动态链接库。与 externals 相比不会生成多个 script 标签,与 splitChunks 相比减少项目打包时的编译解析时间。
第一步,新增 webpack.dll.js 文件。
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
library: ['vue']
},
mode: 'production',
output: {
filename: '[name].dll.[hash].js',
path: path.join(__dirname, 'dist/dll'),
// library 需和 Dllplugin 中的 name 一致
library: '[name]_dll'
},
plugins: [
new webpack.DllPlugin({
name: '[name]_dll',
path: path.join(__dirname, 'dist/dll/[name].json')
})
]
}
第二步,package.json 文件新增命令 npm run dll 命令。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js",
+ "dll": "webpack --config webpack.dll.js"
},
执行命令后,生成 dist/dll 目录,其中的 json 文件用于让 DllReferencePlugin 映射到相关的依赖上。
为了后续打包构建不删除 dll 目录,将 webpack.common.js 中的 clean 修正一下,删除忽略 dll 目录。
clean: {
keep: /dll/
}
第三步,在 webpack.prod.js 文件中的 plugins 字段新增 DllReferencePlugin 配置。
plugins: [
new SpeedMeasureWebpackPlugin(),
new BundleAnalyzerPlugin(),
+ new webpack.DllReferencePlugin({
+ manifest: require('./dist/dll/library.json')
+ })
]
最后一步,在 index.html 上新增 script 标签。
<script defer="defer" src="dll/library.dll.c4b5e421aaf7688e87c9.js"></script>
完成后打包进行测试。
缓存
首先,可将解析 JS 文件的 babel-loader 配置 cacheDirectory 为 true。其次,可配置 TerserPlugin 配置 cache 字段为true(最新版本已兼容缓存功能,4之前可配置)。
最后开启模块缓存,可使用 cache-loader 或者 HardSourceWebpackPlugin 。
cache-loader 使用方式即在开销较大的 loader 前添加 cache-loader,将结果缓存到磁盘中,如放置在 css-loader 之前。
HardSourceWebpackPlugin 无法在 v5 版本下实施,原因与解决方案。
缩小构建目标
首先,比如解析 js、css 等文件时使用 exclude/include 字段确保转义尽可能少的文件,如 include 配置解析 src 目录下的文件或者 exclude 配置 node_modules 目录。
其次,减少文件搜索范围,主要字段是 resolve。
首先是 modules 字段,其默认值是['node_modules']
,目的在于告知 webpack 解析模块时应该搜索的目录,可添加如 path.resolve(__dirname, 'src')
,便于引入公共组件文件import 'components/button'
或者公共类import 'utils'
。
第二是 alias 字段,将原来的模块路径映射成新的导入路径,与resolve.modules
不同,作用在于碰到别名,直接去对应目录下查找模块,减少搜索时间。常见的配置是alias: { '@': path.resolve(__dirname, 'src') }
第三是 mainFields 字段,mainFields 告知 webpack 使用哪个字段来导入模块。web 下默认值为["browser", "module", "main"]
,顺序从左到右,若第三方模块大都采用 main 字段,所以可以将 main 字段放置在最前面,最终根据实际情况来定。
最后是 extensions 字段,目的是自动带入后缀去匹配对应文件。默认值是['js', 'json']
,导入时尽量把后缀名带上,避免查找。
总结
拖得最久的一篇文章,也是 2022 年的第一篇文章。
还有原理篇,喜欢的朋友可以 点赞 + 收藏 + 关注 ~