代码分离
webpack在默认构建的时候,会将所有的代码打包到一个文件中,例如bundle.js
bundle.js中包含往往包含如下内容:
- 自己编写的代码
- 第三方库代码
- webpack为了支持模块化 而添加的额外的运行时代码
而随着业务的发展,bundle.js会越发庞大
-
所有的代码集中在一个文件中,不利于我们对构建后的代码进行管理和维护
-
所有的代码集中到一个文件中,无法做到按需加载或并行加载
用户需要一次性下载全部的代码文件,这会大大降低首屏的渲染速度
所以代码分离(Code Splitting)是webpack一个非常重要的特性
-
它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件
-
比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度
-
代码分离可以分出更小的bundle,以及控制资源加载优先级,提高代码的加载性能
即那些文件需要被下载,那些文件需要在浏览器空闲的时候下载,那些文件是到要使用了才进行下载
常见代码分离方式
- 入口起点
- 防止重复: 使用Entry Dependencies或者SplitChunksPlugin去重和分离代码 --- 使用SplitChunksPlugin进行自定义分离配置
- 动态导入: 通过模块的内联函数调用来分离代码
入口起点
入口起点 就是 使用entry配置手动分离代码, 配置多个入口, 根据多个入口生成多个依赖关系图, 最终打包出多个bundle
entry: {
// name: 路径
main: './src/main.js',
index: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'build'),
// 多入口需要输出多个bundle
// 所以使用placeholder来进行占位 -- 其中name就是entry中配置的key(如name,index)
filename: '[name]-bundle.js',
clean: true
}
但是此时如果我们在多个入口中依赖了重复的模块,那么这些重复的模块会被宰多个入口中重复被打包
所以为了解决这个问题,我们可以单独配置那些模块需要进行共享
entry: {
main: {
// 入口文件
import: './src/main.js',
// 依赖的需要共享的第三方库
dependOn: 'shared1'
},
index: {
import: './src/index.js',
dependOn: 'shared1'
},
// 自定义所需要依赖的第三方库
shared1: 'axios',
shared2: ['dayjs', 'loadsh']
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name]-bundle.js',
clean: true
}
动态导入 (dynamic import)
当我们使用import函数来引入一个模块的时候,
表示我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件
此时浏览器并不会直接下载该模块,而是等到我们真正使用到该模块的时候才去下载该模块
console.log('main')
const btn = document.createElement('button')
btn.textContent = 'index button'
document.body.appendChild(btn)
btn.onclick = () => {
// index.js 并不会立即下载
// 而是等到点击按钮后才会被下载下来
import('./index')
}
我们也可以单独设置chunk的名称
output: {
path: path.resolve(__dirname, 'build'),
// 默认的name
// 如果是bundle - name就是文件名
// 如果是chunk(分的子包) - name是路径_文件名_后缀
filename: '[name]-bundle.js',
// filename 是设置bundle的命名规则
// chunkFilename是设置chunk的命名规则
// 如果没有设置chunkFilename,那么chunk的命名规则采用filename的命名规则
// ps: Filename - 是一个单词
chunkFilename: '[name]-chunk.js',
clean: true
}
// webpackChunkName是魔法注释 -- 也就是给webpack看的注释
// 我们可以使用webpackChunkName来指定对应chunk的名称
// ps: webpackChunkName - chunk和name是两个单词
import(/* webpackChunkName: 'index' */'./index')s
splitChunk
我们可以通过splitChunk来实现自定义webpack分包配置
splitChunk底层是使用SplitChunksPlugin来实现
该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件
只需要提供SplitChunksPlugin相关的配置信息即可
// 优化信息
optimization: {
// 自定义分包信息
splitChunks: {
// 设置那些模块需要分包
// 默认值 async 也就是异步包(即那些import函数)引入的包需要单独分包
// inital 只对同步模块进行打包
// all 表示import方法引入的包和node_modules下被使用的包都需要进行分包
// 第三方包会被打包到vednors-node_modules-xxx-xxxx.js中
chunks: 'all'
}
}
optimization: {
splitChunks: {
chunks: 'all',
// 当包大于20000b的时候,对包进行拆分
// ps: 分出的包可能大于20000b
// 因为webpack在进行拆包的时候,会尽可能的根据依赖所使用的第三方包进行拆分
// 但有的时候拆分完毕后,依旧大于20000b,但是代码已经无法继续进行拆分
// 如一个类或方法是无法进行拆分的,此时拆分出的包就可能大于20000b
maxSize: 20000,
// 拆分后的包不能低于10000b -- 默认值 20000
minSize: 10000
}
}
optimization: {
splitChunks: {
chunks: 'all',
minSize: 10,
// 自定义分包规则
cacheGroups: {
// [name]: 配置对象
vendors: {
// 匹配的正则表达式
// 此处正则匹配的是/node_modules/
// 因为windows下的路径分隔符为\
// 而mac下的是/
// 所以使用[\\/]作为路径分隔符
test: /[\\/]node_modules[\\/]/,
filename: '[name].js'
},
math: {
test: /[\\/]math[\\/]/,
filename: '[name].js'
}
}
}
}
注释提取
默认情况下,webpack在进行分包时,会对包中和版权相关的注释进行单独提取
const TerserPlugin = require('terser-webpack-plugin')
optimization: {
// 可能进行多个优化,如js的优化,css的优化等
// 所以对应的值是数组
minimizer: [
// 抽取版权信息是通过terser-webpack-plugin来实现的
// 而terser-webpack-plugin基于的是terser
new TerserPlugin({
// 不抽取对应的代码
extractComments: false
})
]
}
optimization
chunkIds
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成
也就是用来配置placeholder中id的值是怎么生成的
| 值 | 说明 |
|---|---|
| natural | 按照文件index进行递增形成的数字 文件发生改变的时候,对应的文件名可能发生改变,因为index发生了改变 natural基本不使用 1. 即使文件内容不发生改变,index发生了改变,对应的整个文件也需要重新进行打包 2. 如果名称不变的时候,浏览器会优先使用缓存,而不是请求新文件 但是natural的时候,文件名经常容易发生改变,不利于浏览器的缓存,需要重新下载整个文件 |
| named | 一个可读的名称的id,对应的值和[name]是一致的 development环境下的默认值 |
| deterministic | 在不同的编译中不变的短数字id,也就是说一个文件对应一个数字id production环境下的默认值 |
optimization: {
chunkIds: 'deterministic'
}
runtimeThunk
webpack是模块打包器,所以在构建后会存在一定的runtime代码
runtime相关的代码指的是在运行环境中,负责加载、解析和执行模块,并维护模块之间的依赖关系
我们可以选择在构建后 ,将runtime相关的代码单独抽离,这样可以将业务代码和runtime相关的代码进行分离
这样可以保证如果我们修改了业务代码后,runtime相关的代码是不需要被重新构建,有利于提升构建速度和浏览器缓存
optimization: {
// runtimeChunk可选值
// 1. true/multiple --- 多入口环境下,针对于每一个入口单独打包对应的runtime
// 2. single --- 仅仅只打包一个runtime文件
// 3. 一个有name属性的对象 --- name会作为构建时placeholder中name的值
// runtime构建后的代码是bundlle, output.filename -> [name]-bundle.js
// runtime构建后 则为 runtime-bundle.js
runtimeChunk: {
name: 'runtime'
}
注意: runtime是bundle代码,不是chunk代码
在webpack中,bundle是是指哪些可以直接在浏览器上运行的模块
而chunk是指哪些在bundle代码运行过程中,通过runtime中的模块化代码加载的文件
也就是说chunk文件不能直接在浏览器中执行,而是需要通过bundle进行引入,是bundle文件的一部分
preload 和 prefetch
浏览器在加载页面存在三个步骤
- 下载对应资源
- 加载并解析对应资源
- 渲染解析结果
使用preload和prefetch可以预先下载对应资源,从而减少页面加载和解析所需要的时间
preload和prefetch往往结合import方法一起使用
| 方式 | 名称 | 说明 |
|---|---|---|
| preload | 预加载 | 和主包一起并行下载 preload的资源在主包解析过程中是必不可少的 例如当前页面相关的JS文件和CSS文件 |
| prefetch | 预获取 | 等到所有资源下载完成后 (也就是浏览器空闲时)再去下载对应的资源 prefetch是等空闲后再去加载的资源,所以一般是下一个页面的相关数据和资源 |
import(
/* webpackChunkName: 'math' */
/* webpackPrefetch: true */
'./math/math')
CDN
CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)
- 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器
- 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户
- 来提供高性能、可扩展性及低成本的网络内容传递给用户
CDN服务器一般需要购买,如阿里、腾讯、亚马逊、Google等
但也有一些开源的CDN服务,如unpkg、JSDelivr、cdnjs,bootCDN(国内)等
在开发中,我们使用CDN主要是两种方式
-
打包的所有静态资源,放到自己购买的CDN服务器上,用户所有资源都是通过CDN服务器加载的
-
项目文件依旧存放在自己服务器上,但是一些第三方资源放到CDN服务器上
打包所有静态资源到CDN服务器
<!--
默认情况下,打包后对应的资源是存放在本地的
假设服务器地址为 http://www.example.com
那么请求的路径即为 http://www.example.com/main-bundle.js
-->
<script defer src="runtime-bundle.js"></script>
<script defer src="main-bundle.js"></script>
配置
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name]-bundle.js',
chunkFilename: '[name]-chunk.js',
// 配置所有资源的基准路径
// 默认是 output.path
publicPath: 'https://cdn.example.com',
clean: true
}
此时构建后的文件就变成了
<script defer src="https://cdn.example.com/runtime-bundle.js"></script>
<script defer src="https://cdn.example.com/main-bundle.js"></script>
第三方资源存放到CDN服务器
// 假设项目中使用了react和dayjs
import react from 'react'
import dayjs from 'dayjs'
// webpack.config.js
// externals表示需要再构建的时候排除那些第三方库
// 这是一个和output,resolve,module同级的属性
externals: {
// import react from 'react' 这个库是从react这个包引入的
// 所以这里的key的值是dayjs
// react对应的CDN地址为 https://unpkg.com/react@18.2.0/umd/react.production.min.js
// 在这个CDN地址中所挂载到全局的那个对象是React
// 所以这里的value值是React
dayjs: 'dayjs',
react: 'React'
}
<!-- html的模板文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title></script>
</head>
<body>
<div id="root"></div>
<!--
因为对应第三方已经在构建的时候被移除
所以我们在构建的时候,需要在html模板中手动引入对应的CDN地址
-->
<script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/dayjs.min.js"></script>
</body>
</html>
MiniCssExtractPlugin
MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中
npm install mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// 替代style-loader
// style-loader是将css使用style标签的方式进行插入
// MiniCssExtractPlugin.loader是使用link标签的方式进行插入
MiniCssExtractPlugin.loader,
// 将css解析为js文件,方便webpack进行构建
'css-loader'
]
}
]
},
plugins: [
// 将css文件单独抽离成一个独立的模块
new MiniCssExtractPlugin({
// filename和chunkFilename都可以使用指定文件路径(如css/或js/)的方式来将对应文件打包到某一个文件夹下
// import指令导入的css文件单独抽取到css文件夹下的[name].css
filename: 'css/[name].css',
// import方法导入的css文件单独抽取到css文件相爱的[name]_chunk.css
chunkFilename: 'css/[name]_chunk.css'
})
]
}
placeholder
placeholder是指一种占位符语法,可以在代码中插入动态值
在构建过程中,Webpack会根据配置的规则,将这些占位符替换成对应的值
placeholder中的hash都是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制)
可以通过[hash:8]来控制hash值 的长度
| placeholder | 说明 |
|---|---|
| name | 文件名 -- filname |
| ext | 文件后缀名 --- 带点前缀 |
| id | 为文件生成唯一标识 |
| hash | 基于整个项目生产的hash值 任何一个文件对应的hash值都是一致的 只要项目中有一个文件发送了改变 那么整个项目对应的hash值都会发生改变 不推荐 --- 不利于缓存 |
| chunkhash | 基于分包的chunk内容计算得出的hash值 一个文件foo.js,引入了foo.css 如果使用chunkhash, 那么修改了foo.css或者foo,js中任意一个文件,另一个文件不进行任何修改 foo.css和foo.js对应的hash值都会改变,且他们对应的hash值是一致的 |
| contenthash | 基于文件内容得出的hash值 只有文件本身发生了修改,对应的contenthash才会发生修改 如果引入的文件发送了修改,是不会修改文件本身的contenthash 所以contenthash是被推荐使用的hash模式 |