环境区分
可以定义多个配置文件,通过 webpack-merge
合并配置文件。
安装 webpack-merge
javascript
yarn add webpack-merge
公共配置
javascript
// webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: path.join(__dirname, 'index'),
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: path.join(__dirname, 'src'),
exclude: /node_modules/
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'),
filename: 'index.html'
})
]
};
开发环境配置
javascript
// webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const webpackCommonConf = require('./webpack.common.js');
module.exports = merge(webpackCommonConf, {
mode: 'development',
plugins: [
// 定义环境变量
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('development')
})
],
devServer: {
port: 8080, // 修改端口号
hot: true, // 开启 HMR:开启后 index.html 不会自动刷新
static: {
watch: true, // 自动刷新浏览器
staticOptions: {
progress: true, // 显示打包的进度条
contentBase: path.join(__dirname, 'dist') // 指定运行代码的目录,输出在内存中,看不到
}
},
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
// 设置代理
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
}
});
javascript
// package.json
{
"name": "webpack5-zql",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
},
}
生产环境配置
javascript
// webpack.prod.js
const path = require('path');
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const webpackCommonConf = require('./webpack.common.js');
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
filename: 'bundle.[contenthash:8].js', // 输出文件名,打包代码时加上 hash 戳可以命中缓存
path: path.join(__dirname, 'dist'), // 输出的文件目录
clean: true, //
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
})
]
});
javascript
// package.json
{
"name": "webpack5-zql",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.prod.js"
},
}
提升开发体验
SourceMap
(源代码映射):用来生成源代码与构建后代码一一映射的文件。- 它会生成一个
xxx.map
文件,里面包含换代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过xxx.map
文件,从构建后代码出错的位置找到映射后源代码出错的位置,帮助我们更快的找到错误根源。
通过 webpack.devtool
配置源代码映射。
开发环境
- 开发环境:
cheap-module-source-map
- 打包编译速度快,只包含行映射,没有列映射。
javascript
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map'
};
生产环境
- 生产环境:
source-map
- 包含行、列映射,打包速度编译慢
javascript
module.exports = {
mode: 'production',
devtool: 'source-map'
};
优化打包构建速度
HMR(仅开发环境)
- 开发时,我们修改了其中一个模块代码,
webpack
默认会将所有模块全部重新打包,速度很慢,所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他的模块不变,这样打包速度就可以加快。 Hot Module Replacement
(热模块替换):在程序运行中,替换、添加或删除模块,而无序重新打包整个模块。
javascript
module.exports = {
mode: 'development',
devServer: {
hot: true // 开启 HMR,默认是 true
}
};
-
开发环境下,
css
文件默认支持HMR
,是因为style-loader
做了支持,如果使用MiniCssExtractPlugin.loader
则css
的HMR
会失效,所有开发环境下请使用style-loader
,生产环境下使用MiniCssExtractPlugin.loader
。 -
js
文件默认是不支持HMR
的,修改js
文件依然会全部代码重新打包,如果想让某个js
文件支持HMR
,那么需要在入口文件中写上如下代码:
javascript
if (module.hot) {
// 首先判断是否支持 HMR 功能,因为有些低版本浏览器是不支持的
module.hot.accept('./common/utils/add.js'); // 接收 add.js,一旦 add.js 发生变化,就只加载这个文件
// 由于开发项目这样写起来很麻烦,所以可以采用 loader 自动实现
// 开发 vue 项目可以使用 vue-loader
// 开发 react 项目可以使用 react-hot-loader
}
HMR 原理
webpack-dev-server
会创建两个服务:express
服务(提供静态资源) 和 socket
服务(服务器可以主动发送文件到客户端)。
express server
负责直接提供静态资源的服务:打包后的资源直接被浏览器请求和解析。HMR Socket Server
,是一个socket
的长连接,建立连接后双方可以通信,服务器可以直接发送文件到客户端。而http
请求必须要浏览器主动发起请求。
当服务器监听到对应的模块发生变化时,会生成两个文件。 manifest.json
文件记录更新的位置信息等配置信息,update chunk
.js
文件记录实际更新的具体内容。通过长连接,可以直接将这两个文件主动发送给浏览器,浏览器拿到两个新的文件后,通过 HMR runtime
机制(webpack
在打包的时候提供),加载这两个文件,并且针对修改的模块进行更新。
oneOf
优化生产环境构建打包速度。
正常来讲,一种文件只能被一个 loader
处理。当一种文件要被多个 loader
处理,那么一定要指定 loader
执行的先后顺序:比如处理 js
时,先执行 eslint
再执行 babel
。
假如有 10
个loader
,打包一种文件时就会轮询 10
个 loader
。
如果用了oneOf
,只要匹配到了这个loader
就不会再往后面继续轮询。但是oneOf
里面不能有两个配置处理同一种类型文件,相同的话需要抽出一个放在oneOf
外面,然后指定优先执行。
javascript
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)/,
loader: 'eslint-loader',
enforce: 'pre', // 指定优先执行
},
{
oneOf: [
// oneOf 里的 loader 只会执行一个
{
test: /\.(js|jsx)/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css/,
use: [
'style-css',
'css-loader'
]
}
]
}
]
}
};
include / exclude
开发时我们需要使用第三方库或插件,所有文件都下载到 mode_modules
中了,而这些文件是不需要编译可以直接使用的。
所以我们在对 js
文件处理时,需要排查 node_modules
下面的文件。
include
:包含,只处理 xxx
文件
exclude
:排除,除了 xxx
文件以外的其他文件都要处理
include
和 exclude
只能写一个,要们包含,要么排除。
javascript
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: "mode_modules" // 默认值 mode_modules
}
]
}
};
javascript
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: path.join(__dirname, 'src')
}
]
}
};
cache
每次打包时 js
文件都要经过 eslint
检查和 babel
编译,速度比较慢。
我们可以缓存之前的 eslint
检查和 babel
编译结果,这样第二次打包时速度就会更快了。
cache
- 会对 eslint
检查和 babel
编译结果进行缓存。
javascript
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
include: path.resolve(__dirname, 'src'),
options: {
cacheDirectory: true, // 开启 babel 缓存,会默认缓存到 node_modules/.cache 目录下
cacheCompression: false // 关闭缓存的压缩
}
},
]
},
plugins: [
new EslintPlugin({
context: path.resolve(__dirname, 'src'), // 需要检测的目录
extensions: ['js', 'jsx', 'json'], // 需要检查的文件类型
fix: true, // 自动修复
cache: true, // 开球 eslint 缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache') // 指定缓存的位置
})
]
};
Thread
当项目越来越大时,打包速度越来越慢。
我们想要继续提升打包速度,其实就是要提升 js
的打包速度,因为其他文件都比较少。
而对 js
文件的处理主要是 eslint、babel、Terser
三个工具,所以我们要提升他们的运行速度。我们可以开启多进程同时处理 js
文件,这样速度就比之前的单进程打包更快了。
-
多进程打包:开启电脑的多个进程同时干一件事,速度更快。
-
注意:仅在特别耗时的操作中使用,因为每个进程启动就大约有
600ms
左右的开销。 -
启动进程的数量就是我们
cpu
的核数
javascript
// 由于每个电脑获取 cpu 核数方式都不一样
// 所以使用 nodejs 核心模块来直接使用
const os = require('os'); // 返回 cpu 的一些信息
const threads = os.cpus().length; // cpu 核数
安装 thread-loader
javascript
yarn add thread-loader
- 一般在
babel-loader
进行打包的时候使用,因为处理语法转换很耗时。 - 一般放在需要处理的那个
loader
之后调用。
javascript
const os = require('os');
const threads = os.cpus().length;
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /mode_modules/,
use: [
{
loader: 'thread-loader',
options: {
workers: threads // 开启多进程和设置进程数量
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存,会默认缓存到 node_modules/.cache 目录下
cacheCompression: false // 关闭缓存的压缩
}
}
]
}
]
},
plugins: [
new EslintPlugin({
context: path.resolve(__dirname, 'src'), // 需要检测的目录
extensions: ['js', 'jsx', 'json'], // 需要检查的文件类型
fix: true, // 自动修复
cache: true, // 开球 eslint 环迅
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'), // 指定缓存的位置
threads // 开启多进程和设置进程数量
})
],
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin({
parallel: threads // 开启多进程和设置进程数量
})
]
}
};
减少代码体积
Tree Shaking
开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。如果诶呦特殊处理的话,我们打包时会引入整个库,但是实际上我们可能只用上极小部分的功能,这样把真个库都打包进来,题久就太大了。
Tree Shaking
:通常用于描述移除js
中没有使用上的代码。注意:它依赖es module
。weboack
生产模式下已经默认开启了这个功能,无需其他配置。
减小 babel 体积
babel
为编译的每个文件都插入了辅助代码,使体积过大。
babel
对一些公共方法使用了非常小的辅助代码,比如 _extend
。默认情况下会被添加到每一个需要它的文件中。你可以将这些辅助代码作为一个独立的模块,来避免重复引入。
@babel/plugin-transform-runtime
:禁用了 babel
自动对每个文件的 runtime
注入,而是引入 @babel/plugin-transform-runtime
并且使所有辅助代码从这里引用。
安装 @babel/plugin-transform-runtime
javascript
yarn add @babel/plugin-transform-runtime
javascript
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /mode_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启 babel 缓存,会默认缓存到 node_modules/.cache 目录下
cacheCompression: false, // 关闭缓存的压缩
plugins: ['@babel/plugin-transform-runtime'] // 较少代码体积
}
}
]
}
]
},
};
javascript
// 第二种配置
// .babelrc
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
压缩图片
安装 image-minimizer-webpack-plugin、imagemin
javascript
yarn add image-minimizer-webpack-plugin imagemin -D
无损压缩:下载 imagemin-gifsicle、 imagemin-jpegtran、 imagemin-optipng、 imagemin-svgo
javascript
yarn add imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
有损压缩:下载 imagemin-gifsicle、 imagemin-mozjpeg、 imagemin-pngquant、 imagemin-svgo
javascript
yarn add imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
javascript
module.exports = {
optimization: {
minimize: true,
minimizer: [
// 压缩图片
new ImageMinimizerWebpackPlugin({
minimizer: {
implementation: ImageMinimizerWebpackPlugin.imageminGenerate,
options: {
plugins: [
['gifsicle', { interlaced: true }],
['jpegtran', { progressive: true }],
['optipng', { optimizationLevel: 5 }],
[
'svgo',
{
plugins: [
'preset-default',
'prefixIds',
{
name: 'sortAttrs',
params: {
xmlnsOrder: 'alphabetical'
}
}
]
}
]
]
}
}
})
]
}
};
优化代码运行性能
code split
打包代码时会将所有 js
文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js
文件,其他文件不应该加载。
所以我们需要将打包生成的文件进行代码分割,生成多个 js
文件,渲染哪个页面就只加载某个 js
文件,这样加载的资源就少,速度就更快。
code split
:主要做了两件事。
- 分割文件:将打包生成的文件进行分割,生成多个
js
文件 - 按需加载:需要哪个文件就加载哪个文件
多入口提取文件
正常情况下,有几个入口就会输出几个 bundle
,如果 A
和 B
都引用了模块 C
,那么输出后的 bundleA
和 bundleB
,模块 C
就会分别打包两次。
那么我们可以将 C
单独输出成一个 bundleC
,然后bundleA
和 bundleB
去复用。
javascript
// add.js
const add = (a, b) => a + b;
export default add;
javascript
// index.js
import add from '@utils/add';
console.log(add(1, 6));
javascript
// other.js
import add from '@utils/add';
console.log(add(1, 4));
javascript
const path = require('path');
module.exports = {
mode: 'production',
entry: {
app: './src/index.js',
main: './src/other.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
// 以下是默认值
// minSize: 20000, // 分割代码最小的大小
// minRemainingSize: 0, // 类似于 minSize 最后取保提取的文件大小不能为0
// minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
// mazAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// maxInitialRequests: 30, // 入口 js 文件最大并行请求数量
// enforceSizeThreshold: 50000, // 超过 50kb 一定会单独打包(此时会忽略 minRemainingSize、mazAsyncRequests、maxInitialRequests)
// cacheGroups: { // 分组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
// default: { // 其他没有写的配置会使用默认值
// minChunks: 2, // 这里的 minChunks 权重更大
// priority: -20,
// reuseExistingChunk: true
// }
// },
// 修改配置
cacheGroups: {
default: { // 其他没有写的配置会使用默认值
minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2, // 这里的 minChunks 权重更大
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
多入口按需加载
实现按需加载,动态导入模块。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 class="title">hello webpack5</h1>
<button id="btn">按钮</button>
</body>
</html>
javascript
// add.js
const add = (a, b) => a + b;
export default add;
javascript
// count.js
export const count = (a, b) => a - b;
javascript
// index.js
import add from './add';
console.log(add(1, 6));
javascript
// other.js
document.getElementById('btn').onclick = function () {
// 动态引入 -> 实现按需加载
// 即使只被引用了一次,也会代码分割
import('./count.js').then(({ count }) => {
alert(count(1, 4));
});
};
javascript
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
app: './src/index.js',
main: './src/other.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html'
})
],
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
// 修改配置
cacheGroups: {
default: { // 其他没有写的配置会使用默认值
minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2, // 这里的 minChunks 权重更大
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
单入口提取文件按需加载
javascript
// count.js
export const count = (a, b) => a - b;
javascript
// index.js
document.getElementById('btn').onclick = function () {
// 动态引入 -> 实现按需加载
// 即使只被引用了一次,也会代码分割
import('./count.js').then(({ count }) => {
alert(count(1, 4));
});
};
javascript
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html'
})
],
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
}
}
};
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 class="title">hello webpack5</h1>
<button id="btn">按钮</button>
</body>
</html>
给模块重命名
javascript
// index.js
document.getElementById('btn').onclick = function () {
// 动态引入 -> 实现按需加载
// 即使只被引用了一次,也会代码分割
// 通过注释 webpackChunkName 命名
import(/* webpackChunkName: 'math' */'./count.js').then(({ count }) => {
alert(count(1, 4));
});
};
chunkFilename
使用[name]
(对应webpackChunkName
)
javascript
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[name].js',
chunkFilename: 'chunk/[name].chunk.js', // 给打包输出的其他文件命名
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html'
})
],
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
}
}
};
Preload / Prefetch
使用 code split
可以做代码分割,同时会使用 import
动态导入的语法来进行代码按需加载(懒加载)。但是加载速度还不够好,比如,用户是点击那个按钮才下载资源的,如果资源体积较大,那么用户会感觉到明显的卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源,就需要用上 Preload
和 Prefetch
技术。
-
Preload
:告诉浏览器立即加载资源 -
Prefetch
:告诉浏览器在空闲时才开始加载资源 -
Preload
加载优先级高,Prefetch
优先级低; -
Preload
只加载当前页面需要使用的资源,Prefetch
可以加载当前页面资源,也可以下载一个页面需要使用的资源。 -
都会加载资源,并不执行;都有缓存;兼容性比较差。
安装 @vue/preload-webpack-plugin
javascript
yarn add @vue/preload-webpack-plugin -D
javascript
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin'),
PreloadPlugin = require('@vue/preload-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[name].js',
chunkFilename: 'chunk/[name].chunk.js', // 给打包输出的其他文件命名
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html'
}),
// new PreloadPlugin({
// rel: 'prefetch'
// }),
new PreloadPlugin({
rel: 'preload',
as: 'script'
})
],
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
}
}
};
文件资源缓存
当缓存的资源发生变化时,希望重新加载这个资源;当有一个文件发生变化,只让这一个文件的缓存失效,不影响其他文件缓存。
hash
- 给打包好的文件名添加哈希值,哈希值不变就不会重新打包。
存在问题:因为 js
和 css
同时使用一个hash
值,如果修改了js
,重新打包,会导致所有缓存失效,css
也会重新打包。
javascript
// 给文件名添加hash
module.exports = {
output: {
path: path.resolve(__dirname, '/build'),
// 输出文件带 8 位哈希值,每次重新打包都会生成一个唯一的 hash 值
filename: 'js/built.[hash:8].js'
}
};
chunkhash
- 根据
chunk
生成hash
值,如果打包来源于同一个chunk
,那么hash
值一样。
存在问题:js
和css
的hash
值还是一样的。因为css
是在js
中被引入的,所以同属一个chunk
。
contenthash
contenthash
是根据文件的内容生成hash
值,不同文件hash
值一定不一样。
存在问题:如果 index.js
引入了 count.js
,打包出来的 index.bundle.js
和 count.chunk.js
,如果 修改了 count.js
的内容,那么 index.js 的缓存也会失效。
javascript
module.exports = {
output: {
path: path.resolve(__dirname, '/build'),
// 输出文件带 10 位哈希值,每次重新打包都会生成一个唯一的 hash 值
filename: 'js/built.[contenthash:10].js',
chunkFilename: 'js/chunk/[name].[contenthash:8].chunk.js', // 给打包输出的其他文件命名
}
};
runtime
使用 runtime
就可以避免 contenthash
的问题。
runtimeChunk
:它会把文件之间依赖的hash
值,单独提取出来打包成一个文件去保管。- 当
A
引用B
,B
发生变化,只有B
和runtime
会发生变化,不会影响到A
javascript
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].bundle.js',
chunkFilename: 'js/chunk/[name].[contenthash:8].chunk.js', // 给打包输出的其他文件命名
clean: true
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html'
})
],
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
},
// 把文件之间依赖的 `hash` 值,单独提取出来打包成一个文件去保管
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}.js`
}
}
};