这里写目录标题
- 介绍
- sourcemap定位报错
- 热模块替换(或热替换,HMR)
- oneOf精准解析
- 指定或排除编译
- 开启缓存
- 多进程打包
- 移除未引用代码
- 配置babel,减小代码体积
- 代码分割(Code Split)
- 介绍
- 预获取/预加载(prefetch/preload)
- 静态资源缓存(Network Cache)
- JS兼容
- 渐进式网络应用程序(PWA)
- 资料
介绍
对于webpack配置优化,目的包括但不限于:
- 提升开发体验
- 提升打包速度
- 减少代码体积
- 优化代码运行性能
sourcemap定位报错
实际开发中,会遇到这样一个问题,比如我写一串代码如下:
/*index.js*/
import sum from './sum';
import './index.css';
console.log(sum(1, 2, 3, 4));
console.log(1111)(); // 注意这行,多写了一对(),是错误写法
现在启动开发服务器,在浏览器中查看报错:
它提示,在index.js
的第30
中出现了错误,点击这行报错,在source
中
查看。可以看到,它给提示的第30行,是开发模式下打包后的行数,
并非我们实际代码中的行数。不便于我们快速锁定错误代码和处理bug。
解决这个问题,就需要用到devtool
配置中的sourcemap
配置项。
它可以在打包代码和源代码之间建立一种映射关系,便于排查问题。
缺点是会在一定程度上拖慢打包速度。
文档:
https://www.webpackjs.com/configuration/devtool/
在webpack
配置文件中,配置devtool
字段,它有多个值,
其中,有两个是最常用的:
- 开发模式:
cheap-module-source-map
- 生产模式:
source-map
配置:
module.exports = {
mode: "development",
// ...
devtool: "cheap-module-source-map"
}
// 注:生产模式下会多生成一份.map文件,增加了包的体积
// 另外,建立映射后会增加暴露源码的风险
module.exports = {
mode: "production",
// ...
devtool: "source-map"
}
配置后,重新启动开发服务器,查看报错内容:
此时就很容易看到这一行的报错,和源码完全一致。
热模块替换(或热替换,HMR)
在开发中,每次修改内容后,想查看效果,需要重新打包并刷新页面,
这大大增加了开发耗时。
HMR就解决了这个问题。它能够在修改内容后,只重新打包更新的模块,
无需刷新页面,即可实现页面内容更新。
在配置了devServer
后,会自动开启HMR。
文档:
https://www.webpackjs.com/configuration/dev-server/
但是,这个配置并不支持js代码的热替换,在更新js代码后,会实现
热更新,即页面内容会实时变化,但是页面仍然会刷新。
这需要额外配置。比如:
// index.js
import sum from './sum';
import count from './sum';
if(module.hot) {
module.hot.accept('./sum.js'); // 对sum.js实现热替换
module.hot.accept('./count.js'); // 对count.js实现热替换
}
文档:
https://www.webpackjs.com/api/hot-module-replacement/
很明显,这个写法一点都不美妙,每个文件都要写一次accept。
除此之外,还有其他一些框架的loader可以实现js代码的热替换功能。
比如React
的react-hot-loader
,
文档:
https://github.com/gaearon/react-hot-loader
还有Vue
的vue-loader
,
文档:
https://github.com/vuejs/vue-loader
这些loader配置在脚手架中已经内置了。
oneOf精准解析
在使用很多loader时,都要一个一个的挨个去检查,这个loader是否可以
解析目标文件,会把全部loader都检查一遍。而使用oneOf之后,找到某
个匹配的loader后,就不继续往后找了,降低耗时。类似于switch语句中
的break语句。
文档:
https://www.webpackjs.com/configuration/module/#ruleoneof
配置:
module.exports = {
// ...
module: {
rules: [
{
oneOf: [
{
test: /\.css$/,
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader('sass-loader')
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 10kb
}
}
},
{
test: /\.(ttf|woff2?)$/,
type: 'asset/resource'
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
]
}
}
指定或排除编译
针对js代码,指定处理某些文件或者把某些文件排除,能减少编译文件
数量,加快编译速度。这里需要用到:
- include: 指定编译某些文件
- excluede: 排除某些文件
用其中一个即可。
配置:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
exclude: /node_modules/, // 不编译node_modules中的文件
loader: 'babel-loader'
}
]
}
}
或者:
const path = require('path');
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
// 只编译src目录下的文件
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader'
}
]
}
}
开启缓存
babel和eslint处理js,开启二者的缓存,可以减少js的编译时间。
文档:
https://www.webpackjs.com/loaders/babel-loader/
配置:
const path = require('path');
const EslintPlugin = require('eslint-webpack-plugin');
module.exports = {
mode: 'development',
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
loader: 'babel-loader',
include: path.resolve(__dirname, 'src'),
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false, // 关闭压缩
}
}
]
},
plugins: [
new EslintPlugin({
context: path.resolve(__dirname, 'src'),
cache: true, // 开启缓存功能
// 缓存的eslint文件存放路径
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslintcash')
})
]
}
多进程打包
需要使用的包:
thread-loader
开启多进程,一般放在js相关loader的前面或上面
一般用来处理js或者较大的项目的打包构建。eslint-webpack-plugin
开启eslint多进程检查terser-webpack-plugin
开启多进程压缩,提升打包速度。
这是webpack内置的一个压缩js代码的工具,需要自定义时,仍然需要先安装它。
文档:
# 开启loader的多进程
https://www.webpackjs.com/loaders/thread-loader/
# 开启eslint的多进程
https://www.webpackjs.com/plugins/eslint-webpack-plugin/
# 开启打包的多进程
https://www.webpackjs.com/plugins/terser-webpack-plugin/
现在的电脑都是多核处理器,可以启动多进程打包,在打包一些大型文件时,会节省时间。
需要注意的是,如果项目本身不大,则不必开启多进程打包,因为开启一个进程要耗费600ms.
安装:
npm i thread-loader -D
npm i eslint-webpack-plugin -D
npm i terser-webpack-plugin -D
配置:
const EslintPlugin = require('eslint-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_nodules/,
use: [
{
loader: 'thread-loader',
options: {
workerParallelJobs: 50,
workerNodeArgs: ['--max-old-space-size=4096'],
poolRespawn: false,
poolTimeout: 2000,
poolParallelJobs: 50,
name: "js-pool"
}
},
{
loader: 'babel-loader'
}
]
}
]
},
plugins: [
new EslintPlugin({
context: path.resolve(__dirname, 'src'),
threads: true // 开启多进程
})
],
// 和压缩相关的plugin现在一般写在optimization的minimizer数组中
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true // 开启多进程压缩
})
]
}
}
移除未引用代码
这是用Tree Shaking
概念来处理的。
它依赖于ES6模块,如import
和export
,检测出未被引用的代码,
在打包时,这些代码不会被打包进来。
这是webpack
内置的功能,无需我们配置。
文档:
https://www.webpackjs.com/guides/tree-shaking/
配置babel,减小代码体积
babel在做语法转换时,会在js代码中加入一些辅助函数,随着js文件的增多,
辅助函数的数量也会增大,当这些辅助函数随着一起被打包进来的时候,
打包后的体积就会跟着增加。
要解决这个问题,就需要用到两个插件:
@babel/runtime
用来提供更优雅的辅助函数@babel/plugin-tranform-runtime
它会把所有用到的辅助函数集中到一起,辅助函数数量就大大缩减
文档:
https://www.babeljs.cn/docs/babel-plugin-transform-runtime
https://zhuanlan.zhihu.com/p/394783228
安装:
npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime
配置:
// .babelrc.js
module.exports = {
// ...
plugins: [
"@babel/plugin-transform-runtime"
]
}
或者:
// webpack.config.js
// ...
{
loader: 'babel-loader',
// ...
options: {
plugins: ['@babel/plugin-transform-runtime']
}
}
代码分割(Code Split)
介绍
比如有这样几个文件:
// sum.js
export const str = 'hello';
// index.js
import { str } from './sum';
console.log('index str: ', str);
// main.js
import { str } from './sum';
console.log('main str: ', str);
打包index.js
和main.js
文件,查看打包结果:
// index.js
(()=>{"use strict";console.log("index str: ","hello")})();
// main.js
(()=>{"use strict";console.log("main str: ","hello")})();
可以看到,从sum.js
中引入的str
被分别打包进index.js
和main.js
,相当于sum.js
被打包了两次,
有没有什么方法,可以把sum.js
单独打包,然后让其他文件分别引用,以减小打包体积呢?
另外,当一个文件从sum.js
中引入了str
,是否可以先不调用,只等发生了点击
或者进入了某个页面后再调用呢?
这就引出了代码分割的作用:
- 将打包生成的文件进行分割,生成多个js文件
- 按需加载:需要哪个文件,就加载哪个文件
文档:
https://www.webpackjs.com/guides/code-splitting/
配置:
https://www.webpackjs.com/plugins/split-chunks-plugin/
SplitChunksPlugin
是webpack内置的代码分割插件,开箱即用,无需安装。
默认实现了代码分割功能,也可以自定义配置。
自定义配置:
// webpack.config.js
const path = require("path");
module.exports = {
// 单入口
// entry: './src/index.js',
// 多入口
entry: {
index: "./src/index.js",
main: "./src/main.js",
},
output: {
path: path.resolve(__dirname, "./dist"),
// [name]是webpack命名规则,使用chunk的name作为输出的文件名。
// 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
// chunk的name是啥呢? 比如: entry中xxx: "./src/aaa.js", name就是xxx。
// 注意是前面的xxx,和文件名无关。
// 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件
// 都会叫做main.js会发生覆盖。(实际上会直接报错的)
filename: "js/[name].js",
clean: true,
},
mode: "production",
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 以下是默认值
// 分割代码最小的大小,单位是byte。
// 如果小于这个值,就不会去分割
// minSize: 20000,
// minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
// minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
// maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// maxInitialRequests: 30, // 入口js文件最大并行请求数量
// 超过50kb一定会单独打包
//(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
// enforceSizeThreshold: 50000,
// cacheGroups: { // 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// 如果当前 chunk 包含已从主 bundle 中拆分出的模块,
// 则它将被重用,而不是生成新的模块
// reuseExistingChunk: true,
// },
// default: { // 其他没有写的配置会使用上面的默认值
// minChunks: 2, // 这里的minChunks权重更大
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// 修改配置 要自定义配置,就在这里面修改参数的值
cacheGroups: {
default: {
// 其他没有写的配置会使用上面的默认值
minSize: 20 * 1024, // 小于20kb的文件,不会被分割
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
按需引入要怎么做呢?
需要使用动态引入语法import
比如:
// clickbtn.js
document.querySelector('.btn').onclick = function () {
import('./sum')
.then(res => { console.log('click str: ', res.str) })
.catch(err => { console.log(err) });
}
这样就实现了sum.js
的按需引入,点击才会调用。
再说说chunk.js
文件的命名。
打包后的chunk.js
的名字默认是chunk的id命名的,比如52.js
456.js
,
这里的52和456都是生成的chunk文件的id,我们很难识别它都是谁的打包文件。
因此,当我们需要知道它是谁的打包文件时,就需要给它起个名字。
方法如下:
// 首先是在使用import按需引入时
// 这里的webpackChunkName就是打包sum.js后的chunk名字
// 使用的内联注释激活方法
import(/* webpackChunkName: 'sum' */ './sum.js').then(...)
// 然后在出口里配置:
module.exports = {
// ...
output: {
path: ..
filename: ..
chunkFilename: '[name].chunk.js', //这里的name就是webpackChunkName
}
}
打包后生成的chunk名字就是sum.chunk.js
了。
当然,这种配置也只是在我们想知道是哪个chunk文件打包得到的时候才去配置,
毕竟我们不想每次做import动态引入的时候加上/* webpackChunkName: 'xxx' */
预获取/预加载(prefetch/preload)
文档:
https://www.webpackjs.com/guides/code-splitting/#prefetchingpreloading-modules
https://www.webpackjs.com/plugins/prefetch-plugin/
它们也是代码分割策略的一部分。
prefetch
是在浏览器空闲的时候进行加载,有三种配置实现方式:
第一种:
// 在import动态引入时通过内联注释来实现
import(/* webpackPrefetch: true */ './sum.js').then(...)
第二种:
// 通过配置plugin的方式,内置plugin,无需安装
plugins: [
new webpack.PrefetchPlugin([context], request);
]
第三种:
// 先安装第三方的plugin插件
npm i @vue/preload-webpack-plugin -D
// 引入插件
const PreloadPlugin = require('@vue/preload-webpack-plugin');
// 在webpack里配置插件
module.exports = {
// ...
plugins: [
new PreloadPlugin({
rel: 'prefetch'
})
]
}
静态资源缓存(Network Cache)
作用是当第一次加载过静态资源后,这些静态资源就会被缓存,以后再获取的时候,
就会直接从缓存中获取。
这里需要用到contenthash
和runtime
相关配置。
配置:
module.exports = {
output: {
// ...
filename: '[name].[contenthash:8].js,
chunkFilename: '[name].[contenthash:8].chunk.js
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name],[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].css'
})
],
// ...
runtimeChunk: {
// runtime文件命名
name: (entrypoint) => `runtime~${entrypoint.name}`
}
}
JS兼容
之前配置了@babel/preset-env
用来做做兼容,但是它无法编译es6+语法,需要打补丁polyfill
。
文档:
https://www.babeljs.cn/docs/babel-preset-env
安装:
// 首先,默认之前已经安装了@babel/preset-env
npm i core-js
配置:
// .babelrc.js
module.exports = {
"presets": [
[
"@babel/preset-env",
{
useBuiltIns: "usage", // 用到的依赖打包进来
corejs: {
version: "3.8", // core-js的版本
proposals: true
}
}
]
]
}
渐进式网络应用程序(PWA)
网络离线时也能让应用程序继续运行。
比如你打开了一个网页,突然断网了,这时候如果有pwa功能,依然可以访问该网页。
文档:
https://www.webpackjs.com/guides/progressive-web-application/
安装:
npm i workbox-webpack-plugin -D
配置:
// 1.配置 webpack.config.js
module.exports = {
// ...
plugins: [
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
}
// 2.注册 Service Worker
// 在主文件里(比如index.js)加上下面这段
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
如果打开报错,可以继续如下操作:
安装:
npm i serve -g
启动指令:
serve dist // dist是打包生成的目录
资料
https://yk2012.github.io/sgg_webpack5/intro/