webpack5进阶篇!看完就会辣!
前言
本篇是 webpack 进阶,以写文章的形式学习、巩固相关知识,如果有幸让您阅读,十分荣幸。
本篇将会通过编译工具
、开发体验
、优化打包体积
、提高构建速率
四个方面, 对 webpack 的优化方面的知识进行学习。
您也可以查看我的上一篇文章:webpack基础篇 学习 webpack 的基础配置。
编译工具
编译工具可以提供一些可视化数据,帮助我们分析打包编译的时间、速度、体积等
,方便我们针对性的对项目进行优化。
进度条
我们可以通过 progress-bar-webpack-plugin 插件,在打包时,终端显示进度条
,方便我们掌握打包的进度。
npm i -D progress-bar-webpack-plugin
配置如下:
//webpack.config.common.js
const chalk = require('chalk');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
module.exports = {
//前面的代码...
plugins: [
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
}),
]
}
当我们如上配置后,执行 npm run build 时,就会看见进度条辣!
分析打包速度
打包时,可能会想知道所使用的插件、loader的编译时长,然后针对性的优化,提高构建速率时,我们可以通过 speed-measure-webpack-plugin
插件来实现。
npm install speed-measure-webpack-plugin -D
配置如下:
//webpack.config.common.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// webpack 配置
})
这样,在我们打包时,会看见终端显示插件、loader等工具的打包时间:
分析打包体积
在对项目进行优化的时候,可能会去拆分某些模块,但是又不清楚这些模块的体积,我们就可以通过 webpack-bundle-analyzer
这个插件来辅助我们分析 bundle.js。
npm install webpack-bundle-analyzer -D
配置如下:
//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
],
}
执行 npm run build
后,会自动弹出一个页面:
为我们提供可视化界面,方面我们对打包后的文件模块体积分析。
开发体验
热更新
什么是热更新?热更新就是浏览器不刷新,在不改变页面组件状态的情况下仅更新修改的部分
。
而在 webpack5 中,开发环境的时候,webpack-dev-server 插件可以帮助我们配置热更新。
npm install webpack-dev-server -D
配置如下:
//webpack.config.dev.js
module.exports = {
devServer: {
hot: true //开启热更新
}
}
但仅仅上面配置可能还不够哦!
在实际项目开发过程中,我们修改 less、sass 样式文件时,可以不刷新浏览器的情况下生效,这是因为 less、sass 最终会通过 style-loader 处理,而 style-loader 实现了热模块更新的功能
。
但是当我们在开发(比如 React 项目)时,在 Input 组件输入内容后,修改 index.tsx 后,浏览器会刷新,然后输入框的内容就不会保留,这不是我们希望的。
所以这时候需要 react-refresh-webpack-plugin
和 react-refresh
插件
npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D
配置如下:
//webpack.config.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
plugins: [
new ReactRefreshWebpackPlugin(),
]
}
在 babel.config.js 文件中配置 react-refresh
//babel.config.js
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
"plugins": [
//开发模式下,开启 react 热更新
isDEV && require.resolve('react-refresh/babel)
].filter(boolean)
}
sourceMap
souceMap 是将 源代码与打包后代码 建立映射的配置。它会生成一个 .map
文件,里面包含源代码和构建后代码的映射关系
。这样当代码出错时,我们可以找到问题所在。
souceMap 的配置有多种,区别如何这里就不多说(因为我也不搞不太清也记不住,如果有好的见解、文章,欢迎在评论区评论!)
就我个人而言,我一般是这样配置的:
-
开发模式:
cheap-module-source-map
- 优点:打包编译速度快
-
生产模式:
source-map
-
优点:源代码与构建后代码一一映射
-
配置如下:
//webpack.config.dev.js
module.exports = {
devtool: "cheap-module-source-map",
}
//webpack.config.prod.js
module.exports = {
devtool: "source-map",
}
提高构建速率
cache缓存
webpack 通过配置缓存,会在第一次打包后,缓存文件,当后续打包时,极大的提高构建速率。
配置如下:
//webpack.config.common.js
module.exports = {
cache: {
//文件缓存
type: 'filesystem',
}
}
执行 npm run build
后,可以在 node_modules/.cache/webpack
文件夹下发现一个 default-production 文件夹
同样,当执行 npm run serve
执行运行环境时,在该文件夹下也会生成一个 default-development
alias 别名
通过配置 resolve.alias 别名的方式,减少引用文件的路径复杂度
。
配置如下:
module.exports = {
resolve: {
alias: {
//把 src 文件夹别名为 @
//引入 src 下的文件就可以 import xxx from '@/xxx'
'@': path.join(__dirname, '../src')
}
}
}
减少 loader 作用范围
我们可以通过 exclude
、include
来减轻 loader 的作用范围,提高构建速度
- include:只解析该配置项的模块
- exclude:排除该配置项的模块
配置如下:
//webpack.config.common.js
module.exports = {
module: {
rules: [
{
test: /\.(|ts|tsx)$/,
// 只解析 src 文件夹下的 ts、tsx 文件
// include 可以是数组,表示多个 文件夹下的 模块都要解析
include: path.resolve(__dirname, '../src'),
use: [ 'thread-loader', 'babel-loader']
}
]
}
}
其他的 loader 也可以这样配置减少作用范围。
保证 loader 准确性
loader 在打包过程中,会遍历 rules 数组,根据文件后缀名是否和 test 匹配
,使用对应的 loader 来解析文件。
而在这个过程中,应该避免使用不必要的 loader。如使用 less-loader 去解析 css。
配置如下:
module.exports = {
module: {
rules: [
{
test: /.css$/,
include: [path.resolve(__dirname, '../src')],
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},
{
test: /.less$/,
include: [path.resolve(__dirname, '../src')],
use: [
'style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
},
]
}
}
其他的也是如此,比如 ts、tsx。
extensions
我们在项目中 import 文件时,有时候希望不想去管到底是什么后缀的文件时,可以配置 resolve.extensions
,这样打包时,webpack 会按照我们配置的 extensions 规则去查找对应的文件后缀。
module.exports = {
resolve: {
//extensions 是从左往右解析,所以使用频率高、重要的文件后缀请写在前面
extensions: ['.tsx', '.ts', '.less']
}
}
modules
在项目中,我们会通过 import 大量引入模块,而模块又分为
- node 核心模块,比如
const path = require('path')
- node_modules 模块
- 自定义模块,比如 API 接口模
在引入模块的时候,会以 node 核心模块 -----> node_modules ------> node全局模块
的顺序查找模块。而我们通过配置 resolve.modules 指定 webpack 搜索的范围,快速找到相关模块。
配置如下:
const path = require('path');
module.exports = {
resolve: {
modules: [path.resolve(__dirname, '../node_modules')]
}
}
开启多进程
webpack 的 loader 默认是单线程执行的,而现在的电脑一般都是多核的,所以可以通过开启多进程,帮助 loader 进行解析,减少编译时间,提高构建速率。
我们可以通过 thread-loader 来实现。
npm install thread-loader -D
配置如下:
module.exports = {
modules: {
rules: [
{
test: /(\.jsx|\.js|\.ts|\.tsx)$/,
use: ['thread-loader', 'babel-loader']
}
]
}
}
注意:开启多进程也需要时间。
优化打包体积
在 webpack 打包后,会得到一个 bundle.js 文件,这个文件就是页面执行的脚本,如果 bundle.js 体积过大,页面展现的速率可能就慢,影响用户体验,所以这也是很重要的优化方面。
压缩 JS
打包时,我们可以使用 webpack5 里面自带一个插件 terser-webpack-plugin
来压缩 JS 代码。
//webpack.config.prod.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({ // 压缩js
parallel: true, //开启多进程
//更多配置请看官网
}),
],
},
}
压缩 CSS
我们可以通过 css-minimizer-webpack-plugin 插件来实现。
npm install css-minimizer-webpack-plugin -D
配置如下:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
}
}
压缩图片
我们可以通过 image-minimizer-webpack-plugin
插件来实现图片压缩
npm install image-minimizer-webpack-plugin -D
//webpack.config.prod.js
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
// 压缩图片
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
},
};
抽离 css 代码
我们想将 css 单独打包成一个文件,可以使用 mini-css-extract-plugin
插件。
npm install mini-css-extract-plugin -D
配置如下:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const getStyleLoaders = (preProcessor) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
//在 css-loader 后,配置 postcss-loader
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
}
],
},
plugins: [new MiniCssExtractPlugin()],
};
执行 npm run build
后,dist 目录下就会生成单独的文件 main.css
splitChunk 代码分割
splitChunk 可以帮助我们进行代码分割。
- 比如
第三方库的代码
,一般改动很小,就可以单独打包成一个 chunk; - 比如
公共模块
,提取出公共的避免多次打包增加 bundle.js 的体积;
配置如下:
module.exports = {
// ...
optimization: {
// ...
splitChunks: { // 代码分离
cacheGroups: {
vendors: {
test: /node_modules/, // 把 node_modules 第三方模块拆成单独 chunk
name: 'node_modules.vendors', // 名字是 node_modules.vendors
minChunks: 1, // 至少用一次就切出来
chunks: 'all', // 同步异步的都提取出来
priority: 1, // 优先级
},
commons: { // 公共代码拆成单独的 chunk
name: 'commons', // 名字是 commons
minChunks: 2, // 至少用两次就提取出来
chunks: 'all', // 同步异步的都提取出来
minSize: 0, // 提取代码体积大于0就提取出来
}
}
}
}
}
合理配置文件 hash 值
我们可以通过配置文件的 hash 值,来缓存文件,减少服务器压力,提高页面渲染。
hash 值分为三种:
- hash:跟整个项目有关,只要项目里面有一个文件改变了,则重新生成,且全部文件都用同一个hash
- chunkhash:对应模块的 hash 值,修改对应 chunk 本身、或 chunk 的依赖改变,才会重新生成。
- contenthash:文件自己的 hash 值,文件改动只会影响自己的 hash 值。
配置时,一般是 js 文件以 chunkhash 结尾,图片、css 以 contenthash 结尾
配置如下:
// webpack.config.common.js
module.exports = {
output: {
//js用 chunkhash
filename: 'static/js/[name].[chunkhash:8].js',
},
module: {
rules: [
{
test:/.(png|jpg|jpeg|gif|svg)$/,
generator:{
//图片用 contenthash
filename:'static/images/[name].[contenthash:8][ext]'
},
},
{
test:/.(woff2?|eot|ttf|otf)$/,
generator:{
//字体用 contenthash
filename:'static/fonts/[name].[contenthash:8][ext]',
},
},
{
test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
generator:{
//媒体文件用 contenthash
filename:'static/media/[name].[contenthash:8][ext]',
},
},
]
},
}
tree-shaking
tree-shanking 其实就是在打包的时候,把引用了但没使用的模块给清除掉
JS tree-shaking
webpack5 在 production mode 时,自动开启 tree-shaking
CSS tree-shaking
在 css 里面,也会存在我们写了,但是没有使用的样式,这部分样式对于打包的文件来说就是多余的,得清除掉。
我们可以通过 purgecss-webpack-plugin 来实现。
npm install purgecss-webpack-plugin -D
配置如下:
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const paths = require('paths');
module.exports = {
plugins: [
// CSS 单独打包成一个 文件
new MiniCssExtractPlugin({
filename: "asset/css/main.css",
}),
// CSS Tree Shaking
new PurgeCSSPlugin({
paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),
}),
]
}
懒加载
虽然我们前面通过 splitChunk 进行了代码分离,但是 react、vue 默认会把代码全部打包到 bundle.js 中,而在首页得时候,其实我们只需要首页得代码就行了,其余得等需要加载时再加载即可,可以提高渲染速度。
webpack5 里面默认支持懒加载,通过 import 语法实现。
//在 React 项目中:
//src/component/A.tsx
const A = () => {
return (
<div> AAAA </div>
)
}
export default A;
//src/component/B.tsx
const B = () => {
return (
<div> BBB </div>
)
}
export default B;
//App.tsx
import React, { lazy } from 'react';
const A = Lazy(() => import('@/component/A.tsx'));
const B = Lazy(() => import('@/component/B.tsx'));
function App() {
return (
<div>
//引入了 A、B 子组件,但是只使用了 A 组件
<A />
</div>
)
}
export default App
在打包后,bundle.js 里面只有 A 组件的内容,不会有 B 组件的。
预加载
预加载可以通过 preLoad
、preFetch
来实现。
- preload:告诉浏览器,当前页面
必须加载
的资源,即一定会去加载这些资源 - prefetch:告诉浏览器,当前页面
可能需要
的资源,不一定现在需要,有空时去加载。
在 React 项目中,我们可以通过 import + webpack 魔法注释
实现。
// src/components/A.tsx
import React from "react";
function A() {
return (
<div> AAA </div>
)
}
export default A
// src/components/B.tsx
import React from "react";
function A() {
return (
<div> BBB </div>
)
}
export default B
// App.tsx
import React, { lazy, Suspense, useState } from 'react'
// prefetch
const A = lazy(() => import(
/* webpackChunkName: "preFetchComponent_A" */
/* webpackPrefetch: true*/
'@/components/A'
))
// preload
const B = lazy(() => import(
/* webpackChunkName: "preLoadComponent_B" */
/* webpackPreload: true*/
'@/components/B'
))
function App() {
const [ visible, setVisible ] = useState(false)
const onClick = () => {
setShow(true)
}
return (
<>
<div onClick={onClick}>show</div>
{ visible && (
<>
<A />
<B />
</>
) }
</>
)
}
export default App
打包后,页面初始化的时候就会预加载 A 组件的资源,直接加载 B 组件的资源
。当点击了 show 之后,A组件中的代码才执行
。
结尾
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。
转载自:https://juejin.cn/post/7226267942148390949