webpack 总结
webpack.config.js 文件配置
-
mode
开发模式 development 和 production 默认 production 区分环境
- --mode 用来设置模块内的 process.env.NODE_ENV
- --env 用来设置 webpack 配置文件的函数参数
- cross-env 用来设置 node 环境的 process.env.NODE_ENV
- DefinePlugin 用来设置模块内的全局变量(插件)
-
entry
入口 指示 webpack 应用哪个模块 作为用来构建其内部依赖图的开始文件, webpack 会找出有那些模块和库是入口文件直接或者间接依赖的 可以配置一个对象 来执行多个入口
-
output
输出文件 可以是一个对象 path 是文件路径 filename 是文件名称 可以指定 hash chunksHash contenthash 来监控变化 publicPath 文件前缀
-
loader
因为 webpack 只能解析 js 和 JSON 文件 loader 可以让 webpack 处理其他类型文件 并转换成 js 或者 JSON 文件 以供 webpack 使用
-
plugin
loader 的升级版 loader 用于转换某些类型的模块 而插件可以用于执行范围更广的任务 包括打包优化 资源管理 注入环境变量
-
devServer
开发服务器
- static 静态资源文件位置
- port 端口号
- open 启动是否默认打开
- proxy 代理路径 值也可以是一个对象 代理二级目录
- onBeforeSetupMiddleware 中间件 拦截内容返回特定内容 可实现简单的 mock 数据
-
resolve
- alias 别名映射
-
devtool
是否生成 source map
-
Module
-
rule.enforce 指定是哪种 loader 执行
- 一共四种 后置 内联 正常 前置
- 每种 loader 都有两个阶段
- pitching 阶段 执行 loader 上的 pitch 方法 顺序为 后置 内联 正常 前置
- Normal 阶段 执行 loader 的常规方法 顺序为 前置 正常 内联 后置
- 每一个 loader 都有一个 options 可以给这个 loader 单独配置一些插件
-
presets 预设包
-
plugins 插件集
-
打包文件解析
-
基础使用
- modules 对象
来区分每个导入文件和文件的内容 key 是每个模块相对于根目录的相对路径 value 是一个函数 作用是给对应 module 导出对应的内容
- cache 缓存对象
- require 导出方法
和 Node 的 require 一样 传入一个路径 导出这个路径文件的内容
- modules 对象
-
兼容 common 和 EsMoudle 相互加载 原则 最终导出的都是 commonjs
require.r 给模块加个标识 __esModule 和一个自定义标签 Symbol.toStringTag({value:'Module'})
- Symbol.toStringTag object Module
是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签
- require.n
判断 module 是 EsModule 还是 Common 来返回不同的值 如果是 esModule 则返回 module.default 是 common 则返回 module
- Symbol.toStringTag object Module
-
异步加载其他模块
-
需要定义一个新的对象 来存储已加载的模块名 key 是代码块名称 value:0 就表示加载完了 ,定义一个加载 chunk 模块的数组 挂载到 window 上 重写 push 方法,
-
push 方法拿到需要加载的代码块 ID 放到队列中等待加载,
-
异步加载通过 promise 的方式来进行加载,
-
通过 JSONP 原理异步加载一个 chunkId 对应的代码块文件,
-
生成一个 promise 的成功态和失败态,
-
把每个代码块的 reslove 执行 并把代码块的值改为 0 标识为加载完成,
-
并创建一个 script 脚本加载到 heand 中 url 就是这个异步加载模块的地址
-
ast 语法树
- AST 是深度优先遍历原则
- parser 是把 JavaScript 源码转化为抽象语法树的解析器
- AST 语法树节点分类
- File 文件
- Program 程序
- Literal 字面量 NumericLiteral StringLiteral BooleanLiteral
- Identifier 标识符
- Statement 语句
- Declaration 声明语句
- Expression 表达式
- Class 类
- 常用插件
- esprima 把 JS 源代码转成 AST 语法树
- estraverse 遍历语法树,修改树上的节点
- escodegen 把 AST 语法树重新转换成代码
- 都是通过访问者模式 Visitor 来判断不同的节点类型进行不同的操作
babel 编译
- 作用 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行
- 工作过程分为三步
- Parse(解析) 将源代码转换成抽象语法树,
- Transform(转换) 对抽象语法树进行转换
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
- babel 插件
- @babel/parser 可以把源码转换成 AST
- @babel/traverse 用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
- @babel/generate 可以把 AST 生成源码,同时生成 sourcemap
- @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
- @babel/template 可以简化 AST 的创建逻辑
- @babel/code-frame 可以打印代码位置
- @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
- babel 插件实现 //todo
- path.resolve(__dirname, 'plugins/babel-logger.js'), 指定自己写的插件地址
- callee 在这里有使用
webpack 工作流程
-
调试能力
-
通过 chrome 调试
- node --inspect-brk ./node_modules/webpack-cli/bin/cli.js
-
通过执行名称调试
打开 vscode 点击调试按钮 点击小齿轮 生成 lauch.json 文件 修改完文件内容 直接 F5 开启
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "debug webpack", "cwd": "${workspaceFolder}", "program": "${workspaceFolder}/node_modules/webpack-cli/bin/cli.js" } ] }
-
-
通过 debugger.js
自己创建一个 debugger.js 文件 导入 webpack 和 webpack 配置文件 再执行
const webpack = require("webpack"); const webpackOptions = require("./webpack.config"); const compiler = webpack(webpackOptions); //4.执行对象的run方法开始执行编译 compiler.run((err, stats) => { console.log(err); console.log( stats.toJson({ assets: true, chunks: true, modules: true, }) ); }); -
webpack 编译流程
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化 Compiler 对象
- 加载所有配置的插件
- 执行 Compiler 对象的 run 方法开始执行编译
- 根据配置中的 entry 找出入口文件
- 从入口文件出发,调用所有配置的 Loader 对模块进行编译
- 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
-
stats 对象
- 在 Webpack 的回调函数中会得到 stats 对象
- 这个对象实际来自于 Compilation.getStats(),返回的是主要含有 modules、chunks 和 assets 三个属性值的对象。
- modules 记录了所有解析后的模块
- chunks 记录了所有的 chunk
- assets 记录了所有要生成的文件
Loader 运行流程
- loader 的编译发生在 webpack 流程的编译模块过程(6-7 步)
- 每一个 loader 编译模块都会经历的阶段
- pitch 阶段 这个阶段还没有达到目标文件 但是参数中会默认有后面的文件路径
- normal 阶段 这是正常的 loader 编辑过程
- 每个阶段都会有 后置 内联 正常 前置 4 个类型的 loader 是 enforce 这个 kay 来指定的
- 完成的运行流程是 pitch(后置)->pitch(内联)->pitch(正常)->pitch(前置)->目标文件->normal(前置)->normal(正常)->normal(内联)->normal(后置)
- 这两个阶段的 loader 如果有返回值的话 都会作为下个参数的入参
- pitch 有返回值的话 不会在执行下一个 pitch 而是会直接走到 pitch 上一个的 normal 节点
- 内联配置中可以通过特殊标识符来控制要不要前置或者后置或者普通 loader
- -! 不要前置和普通 loader
- ! 不要普通 loader
- !! 不要前后置和普通 loader,只要内联 loader
- pitch 在 style-loader 中会使用
- 同时 loader 的返回值可以分为两种 一种是 js 代码 可以被用在最左侧提供给 webpack 使用 还有的是不能被放在最左边的 loader
tapable
类似于 Node 的 EventEmitter 但更专注于自定义事件的触发和处理 webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在 webpack 中最核心的负责编译的 Compiler 和负责创建 bundle 的 Compilation 都是 Tapable 的实例
-
tapable 很多核心方法
-
按照同步异步来分类
- sync 同步
- SyncHook 同步钩子
- SyncBailHook 同步保障钩子
- SyncWaterfailHook 同步瀑布钩子
- Async 异步
- AsyncParallel 异步并行
- AsyncParallelHook 异步并行钩子
- AsyncParallelBailHook 异步并行保障钩子
- AsyncSeries 异步串行
- AsyncSeriesHook 异步串行钩子
- AsyncSeriesBailHook 异步串行保障钩子
- AsyncSeriesWaterfailHook 异步瀑布钩子
- AsyncParallel 异步并行
- sync 同步
-
按照返回值来分类
- Basic 执行每一个事件函数,不关心函数的返回值
- SyncHook
- AsyncParallelHook
- AsyncSeriesHook
- Bail 执行每一个事件函数,遇到第一个结果 result !== undefined 则返回,不再继续执行
- SyncBailHook
- AsyncSeriesBailHook
- AsyncParallelBailHook
- Waterfall 如果前一个事件函数的结果 result !== undefined,则 result 会作为后一个事件函数的第一个参数
- SyncWaterfallHook
- AsyncSeriesWaterfallHook
- Loop 不停的循环执行事件函数,直到所有函数结果 result === undefined
- SyncLoopHook
- AsyncSeriesLoopHook
- Basic 执行每一个事件函数,不关心函数的返回值
-
基本使用
- 所有的构造函数都接收一个可选参数,参数是一个参数名的字符串数组
- 参数的名字可以任意填写,但是参数数组的长数必须要根实际接受的参数个数一致
- 如果回调函数不接受参数,可以传入空数组
- 在实例化的时候传入的数组长度长度有用,值没有用途
- 执行 call 时,参数个数和实例化时的数组长度有关
- 回调的时候是按先入先出的顺序执行的,先放的先执行
const { SyncHook } = require("tapable"); const hook = new SyncHook(["name", "age"]); hook.tap("1", (name, age) => { console.log(1, name, age); return 1; }); hook.tap("2", (name, age) => { console.log(2, name, age); return 2; }); hook.tap("3", (name, age) => { console.log(3, name, age); return 3; }); hook.call("zhufeng", 10); -
interceptor 拦截器
-
stage 阶段 强行排序 类似于 router 的 store
-
bofore 一个数组 表示要在他之前执行
plugin 插件
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来做相应的钩子
-
使用场景
- webpack 基础配置已经无法满足需求
- 插件几乎能够任意改变 webpack 的编译结果
- webpack 本身也是通过各种内置插件来组装完成的
-
插件是一个 class 类 类上有一个 apply 方法 方法的参数就是 compiler
-
Compiler
- compiler 对象代表了完整的 webpack 环境配置。
- 这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。
- 当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
-
Compilation
- compilation 对象代表了一次资源版本构建。
- 当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。
- 一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。
- compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
-
-
手写插件的基本使用
//编写个Compilation插件,用来打印本次产出的代码块和文件 class WebpackAssetsPlugin { constructor(options) { this.options = options; } apply(compiler) { //每当webpack开启一次新的编译 ,就会创建一个新的compilation compiler.hooks.compilation.tap("WebpackAssetsPlugin", (compilation) => { //每次根据chunk创建一个新的文件后会触发一次chunkAsset compilation.hooks.chunkAsset.tap( "WebpackAssetsPlugin", (chunk, filename) => { console.log(chunk.name || chunk.id, filename); } ); }); } } module.exports = WebpackAssetsPlugin;
代码分割
-
对于大的 web 应用来说 把所有的代码都放到一个文件显然是不够有效加载的,特别是一些按需加载的代码
- webpack 有一个功能就是吧你的代码库分割成 chunks 语块 当代码运行到需要的时候再进行加载
-
webpack 本身自带入口分割 也就是 entry 设置多个入口文件 但是有问题
如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中
不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
-
动态导入和懒加载
- 用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时
- 一般采用以下原则
- 对网站功能进行划分,每一类一个 chunk
- 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
- 被分割出去的代码需要一个按需加载的时机
- preload 预先加载
- preload 通常用于本页面要用到的关键资源,包括关键 js、字体、css 文件
- preload 将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度
- 在资源上添加预先加载的注释,你指明该模块需要立即被使用
- 一个资源的加载的优先级被分为五个级别,分别是
- Highest 最高
- High 高
- Medium 中等
- Low 低
- Lowest 最低
- 异步/延迟/脚本无论在什么位置 在网络中的优先级都是 low
- @vue/preload-webpack-plugin 插件
- prefetch 预先拉取
- prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
-
提取公共代码
- 为什么需要提取公共代码
- 大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共代码,如果都包含进来会有问题
- 相同的资源被重复的加载,浪费用户的流量和服务器的成本;
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
- 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本
- 如何提取
- 基础类库 方便长期缓存
- 页面之间的公用代码
- 各个页面单独生成文件
- module chunk bundle 的关系
- module:就是 js 的模块化 webpack 支持 commonJS、ES6 等模块化规范,简单来说就是你通过 import 语句引入的代码
- chunk: chunk 是 webpack 根据功能拆分出来的,包含三种情况
- 你的项目入口(entry)
- 通过 import()动态引入的代码
- 通过 splitChunks 拆分出来的代码
- bundle bundle 是 webpack 打包之后的各个文件,一般就是和 chunk 是一对一的关系,bundle 就是对 chunk 进行编译压缩打包等处理之后的产出
- 个人理解 bundle 是各个文件 chunk 是 bundle 里的 push 对象 包含了 chunkId 和 module , module 是编译之后的代码
- bundle > chunk > module
- splitChunks optimization.splitChunks
- 将 optimization.runtimeChunk 设置为 true 或 'multiple',会为每个入口添加一个只含有 runtime 的额外 chunk
- splitChunks 属性介绍
- chunks: 'all' 表示选择哪些 chunks 进行分割,可选值有:async,initial 和 all
- minSize:0 表示新分离出的 chunk 必须大于等于 minSize,默认为 20000,约 20kb。
- minChunks: 1, 表示一个模块至少应被 minChunks 个 chunk 所包含才能分割。默认为 1。
- maxAsyncRequests:3 表示按需加载文件时,并行请求的最大数目。默认为 5
- maxInitialRequests:5 表示加载入口文件时,并行请求的最大数目。默认为 3
- automaticNameDelimiter: '
', // 表示拆分出的 chunk 的名称连接符。默认为。如 chunk~vendors.js - cacheGroups 缓存组 有两个默认缓存对象
- defaultVendors
- test 满足条件进到这个组里 基本上是正则写法 比如 /[\/]node_modules[\/]/
- priority :-10 优先级 一个 chunk 很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认 0),默认缓存组的 priority 属性为负值
- minChunks: 2, 被多少模块共享,在分割之前模块的被引用次数
- reuseExistingChunk 如果当前的代码包含已经被从主 bundle 中分割出去的模块,它将会被重用,而不会生成一个新的代码块
- default
- xxx 自定义缓存组
- output.publicPath: auto 自行匹配域名
- 为什么需要提取公共代码
HMR 热更新模块替换
- 指当我们对代码修改并保存后,webpack 将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面
- HotModuleReplacementPlugin 一个 webpack 内置插件
- webpack\lib\HotModuleReplacementPlugin.js
- 他会在入口文件里插入两个补丁包
- 上一次编译生成的 hash.hot-update.json,说明从上次编译到现在哪些代码块发生成改变
- chunk 名字.上一次编译生成的 hash.hot-update.js,存放着此代码块最新的模块定义,里面会调用 webpackHotUpdate 方法
- 向代码块中注入 HMR runtime 代码,热更新的主要逻辑,比如拉取代码、执行代码、执行 accept 回调都是 webpackHotUpdate 注入的到 chunk 中的
- hotCreateRequire 会帮我们给模块 module 的 parents、children 赋值
- webpack 的监控模式
- 如果使用监控模式编译 webpack 的话,如果文件系统中有文件发生了改变,webpack 会监听到并重新打包
- 每次编译会产生一个新的 hash 值
- 工作流程
- 服务器部分
- 启动 webpack-dev-server 服务器
- 创建 webpack 实例
- 创建 Server 服务器
- 添加 webpack 的 done 回调 在编译完成后向浏览器发送消息
- 创建 express 应用 app
- 使用监控模式开始启动 webpack 编译, 在 webpack 的 watch 模式下 文件系统的某一项文件变化,webpack 监听到文件变化,根据配置文件对模块重新打包编译 并将打包后的代码通过简单的 js 对象保存在内存中
- 设置文件系统为内存文件系统
- 添加 webpack-dev-middleware 中间件
- 创建 http 服务器并启动服务
- 使用 sockjs 在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换
- 客户端部分
- 连接 websocket 服务器
- websocket 客户端监听事件 监听 hash 事件 保存此 hash 值
- 监听 ok 事件 执行 reloadApp 方法进行更新
- 在 reloadApp 中会进行判断 是否支持热更新, 如果支持的话就发射 webpackHotUpdate 事件 如果不支持就直接刷新浏览器
- 在 webpack/hot/dev-sever.js 中会监听 webpackHotUpdate 事件
- 在 check 方法里调用 module.hot.check 方法
- 调用 hotDownloadManifest,向 server 端发送 Ajax 请求,服务端返回一个 Manifest 文件(lastHash.hot-update.json),该 Manifest 包含了本次编译 hash 值 和 更新模块的 chunk 名
- 调用 JsonpMainTemplate.runtime 的 hotDownloadUpdateChunk 方法通过 JSONP 请求获取到最新的模块代码
- 补丁 JS 取回来后会调用 JsonpMainTemplate.runtime.js 的 webpackHotUpdate 方法
- 然后会调用 HotModuleReplacement.runtime.js 的 hotAddUpdateChunk 方法动态更新模块代码
- 然后调用 hotApply 方法进行热更新
- 从缓存中删除旧模块
- 执行 accept 的回调
- 服务器部分
sourceMap
由 5 个基础配置参数组合而成
{
"version":3,
"file":"script-min.js",
"lineCount":1,
"mappings":"AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE;",
"sources":["script.js"],
"names":["a","b","c"]
}
- 基础参数配置
- source-map 生成.map 文件
- eval 使用 eval 包裹模块代码 而不会生成 Map
- cheap 不包含详细的列信息
- module 包含 loader 的 sourcemap 否则法务定义源文件
- inline 将.map 文件作为 dataURL 嵌入 不会单独生成.map 文件
- 最佳实践
- 开发环境 devtool: cheap-module-eval-source-map 快(eval),信息全(module)
- 生产环境 devtool: hidden-source-map 一方面 webpack 会生成 sourcemap 文件以提供给错误收集工具比如 sentry,另一方面又不会为 bundle 添加引用注释,以避免浏览器使用
- map 文件分析
- version source map 的版本 目前都是 3
- file 转换后的文件名
- sourceRoot 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空 基本看不到
- sources 转换前的文件集合 一个数组
- names 转换前的所有变量名和属性名
- mappings 记录位置信息的字符串
- mappings 解析
- 这是一个很长的字符串,它分成三层
- 第一层是行对应 以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。 基本都是一层 因为源文件里的都是压缩成一行了
- 第二层是位置对应 以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
- 第三层是位置转换 以 VLQ 编码表示,代表该位置对应的转换前的源码位置。
- 位置对应的原理
- 每个位置使用五位,表示五个字段
- 第一位 表示这个位置在(转换后的代码的)的第几列
- 第二位 表示这个位置属于 sources 属性中的哪一个文件 不用关注
- 第三位 表示这个位置属于转换前代码的第几行
- 第四位 表示这个位置属于转换前代码的第几列
- 第五位 表示这个位置属于 names 属性中的哪一个变量 不用关注
首先,所有的值都是以 0 作为基数的。其次,第五位不是必需的,如果该位置没有对应 names 属性中的变量,可以省略第五位,再次,每一位都采用 VLQ 编码表示;由于 VLQ 编码是变长的,所以每一位可以由多个字符构成 如果某个位置是 AAAAA,由于 A 在 VLQ 编码中表示 0,因此这个位置的五个位实际上都是 0。它的意思是,该位置在转换后代码的第 0 列,对应 sources 属性中第 0 个文件,属于转换前代码的第 0 行第 0 列,对应 names 属性中的第 0 个变量。
- 相对位置
对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述 第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少
- 编码格式
- VLQ 编码
- VLQ 是 Variable-length quantity 的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式
- 这种编码需要用最高位表示连续性,如果是 1,代表这组字节后面的一组字节也属于同一个数;如果是 0,表示该数值到这就结束了
- 如何对数值 137 进行 VLQ 编码
- 将 137 改写成二进制形式 10001001
- 七位一组做分组,不足的补 0 0000001 0001001
- 最后一组开头补 0,其余补 1 10000001 00001001
- 137 的 VLQ 编码形式为 10000001 00001001
- Base64 VLQ
- 一个 Base64 字符只能表示 6bit(2^6)的数据
- Base64 VLQ 需要能够表示负数,于是用最后一位来作为符号标志位
- 由于只能用 6 位进行存储,而第一位表示是否连续的标志,最后一位表示正数/负数。中间只有 4 位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了
- 表示正负的方式
- 如果这组数是某个数值的 VLQ 编码的第一组字节,那它的最后一位代表"符号",0 为正,1 为负
- 如果不是,这个位没有特殊含义,被算作数值的一部分
- 在 Base64 VLQ 中,编码顺序是从低位到高位,而在 VLQ 中,编码顺序是从高位到低位
- VLQ 编码
- 每个位置使用五位,表示五个字段
webpack5 新特性
- 最重要 3 个 持续化缓存 tree-shaking(删除无依赖的 js 代码) 模块联邦
- 持续化缓存
- 缓存生成的 webpack 模块和 chunk,来改善构建速度
- cache 会在开发模式被设置成 type: 'memory' 而且在 生产 模式 中被禁用
- 在 webpack5 中默认开启,缓存默认是在内存里,但可以对 cache 进行设置
- 当设置 cache.type: "filesystem"的时候,webpack 会在内部启用文件缓存和内存缓存,写入的时候会同时写入内存和文件,读取缓存的时候会先读内存,如果内存里没有才会读取文件
- 每个缓存最大资源占用不超过 500MB,当逼近或超过 500MB 时,会优先删除最老的缓存,并且缓存的有效期最长为 2 周
- 默认情况下,webpack 假定 webpack 所在的 node_modules 目录只被包管理器修改。对 node_modules 来说,哈希值和时间戳会被跳过
- cache 为 filesystem 的时候 不能使用 cnpm 来安装包依赖 因为 cnpm 的包名有冲突 webpack5 规范包名如果存在@就会以@开头 而 cnpm 的包名是_@ 有个判断进不去 会卡死 或者导致栈溢出
- (问题复现)[github.com/cnpm/cnpm/i…]
- 资源模块内置化
- asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
- asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
- asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
- asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现
- URLs
- 支持在请求中处理协议
- 支持 data 支持 Base64 或原始编码,MimeType 可以在 module.rule 中被映射到加载器和模块类型
- 支持 http(s)
import data from "data:text/javascript,export default 'title'";
- moduleIds & chunkIds 的优化
- webpack5 之前非入口文件的打包都是以 import 的引入顺序用阿拉伯数字 1,2,3 的来命名 有一个文件的引入改变 所有之后的都要变化
- webpack5 之后 生产环境 默认是 deterministic 以模块名称+路径生成一个 hash 值 有个 bug 文件引入超过 999 个 会有 Hash 值导致的命名冲突 开发环境默认是 named 方便调试的高可读性 id
- 可以通过 optimization.moduleIds 和 optimization.chunkIds 来执行属性值
- false 不应使用任何内置算法,插件提供自定义算法
- natural 按使用顺序的数字 ID
- named 方便调试的高可读性 id
- deterministic 根据模块名称生成简短的 hash 值
- size 根据模块大小生成的数字 id
- 删除 Node.js 的 ployfill
- reslove.fallback
- 优化点不大
- 延伸知识 webpack 的作者本身是写 java 的 webpack 也可打包服务端代码 Tobias
- 更强大的 tree-shaking 删除无用代码机制
- webpack4 本身的 tree shaking 比较简单,主要是找一个 import 进来的变量是否在这个模块内出现过,非常简单粗暴
- 原理
- webpack 从入口遍历所有模块的形成依赖图,webpack 知道那些导出被使用
- 遍历所有的作用域并将其进行分析,消除未使用的范围和模块的方法
- webpack-deep-scope-analysis-plugin 深层遍历插件
- optimization.usedExports 开启
- package.json 里配置 sideEffects:false 表示是纯函数 只要没使用就会被删除
- sideEffects: ["*.css"] 不过滤 css 文件
优化点
-
缩小范围 resolve
- extensions 指定 extension 之后可以不用在 require 或是 import 的时候加文件扩展名,会依次尝试添加扩展名进行匹配
- alias 配置别名用来加快 webpack 查找模块的速度
- modules 对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样进行路径搜索 如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话可以指定 modules: [path.resolve(__dirname, 'node_modules')],
- mainFields 当从 npm 导入模块时 此选项将决定在 package.json 中使用哪个字段导入模块
- mainFiles 目录中没有 package.json 默认使用目录下的 index.js 这个文件
- resolveLoader 解析 loader 时的 resolve 配置
-
noParse module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析
- 基本用在第三方大型类库 且没有依赖 可以通过这个字段配置 以提高整体的构建速度
- 使用 noParse 的模块内部不用有 import require define 等导入机制
-
IgnorePlugin 用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去
// 插件 new webpack.IgnorePlugin({ //匹配引入模块路径的正则表达式 contextRegExp: /moment$/, //匹配模块的对应上下文,即所在目录名 resourceRegExp: /^\.\/locale/, }); -
费时检查 webpack-bundle-analyzer 生成代码分析报告 查看代码体积
-
output 的 libraryTarget 和 library
- 当使用 webpack 构建一个可以被其他模块导入的库时需要使用他们
- libraryTarget 的类型
- var 默认类型 只能通过 script 标签来引入
- commonjs 只能按照 commonjs 规范导入
- commonjs2 只能通过 commonjs2 规范导入
- amd 只能按照 amd 规范导入
- umd 可以通过 script commonjs amd 引入
-
单独提取 css mini-css-extract-plugin
- 指定目录
new MiniCssExtractPlugin({ filename: 'css/[name].css' }), -
压缩和优化 css、js、html 资源
- css 压缩 optimize-css-assets-webpack-plugin 是一个优化和压缩 CSS 资源的插件
- js 压缩 optimization.minimizer 覆盖 webpack 的默认压缩插件
- js 压缩 terser-webpack-plugin 是一个优化和压缩 JS 资源的插件
- html 压缩 HtmlWebpackPlugin 中配置 minify 对象中 collapseWhitespace removeComments 为 true
- purgecss-webpack-plugin
- 可以去除未使用的 css,一般与 glob、glob-all 配合使用
- 必须和 mini-css-extract-plugin 配合使用
-
cdn
- 使用缓存
- 静态的 JavaScript、CSS、图片等文件开启 CDN 和缓存,并且文件名带上 HASH 值
- 为了并行加载不阻塞,把不同的静态资源分配到不同的 CDN 服务器上
- 域名限制
- 同一时刻针对同一个域名的资源并行请求是有限制 可以把这些静态资源分散到不同的 CDN 服务上去 多个域名后会增加域名解析时间
- 可以通过在 HTML HEAD 标签中 加入 link rel="dns-prefetch" href="xxx"去预解析域名,以降低域名解析带来的延迟
- 文件指纹
- 打包后输出的文件名和后缀
- hash 一般是结合 CDN 缓存来使用,通过 webpack 构建之后,生成对应文件名自动带上对应的 MD5 值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的 HTML 引用的 URL 地址也会改变,触发 CDN 服务器从源服务器上拉取对应数据,进而更新本地缓存。
- 指纹占位符
- ext 资源后缀名
- name 文件名称
- path 文件的相对路径
- folder 文件所在的文件夹
- hash 每次 webpack 构建时生成一个唯一的 hash 值
- chunkHash 根据 chunk 生成 hash 值,来源于同一个 chunk,则 hash 值就一样
- contenthash 根据内容生成 hash 值,文件内容相同 hash 值就相同
- hash 是指的每个文件的 hash 既有任何一个文件修改 所有文件的 hash 都会发生变化
- chunkHash chunkhash 和 hash 不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用 chunkhash 的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响
- contentHash 使用 chunkhash 存在一个问题,就是当在一个 JS 文件中引入 CSS 文件,编译后它们的 hash 是相同的,而且只要 js 文件发生改变 ,关联的 css 文件 hash 也会改变,这个时候可以使用 contenthash 值,保证即使 css 文件所处的模块里就算其他文件内容改变,只要 css 文件内容不变,那么不会重复构建
- HashPlugin 插件 自定义 hash 值
- 使用缓存
-
moduleIds & chunkIds 的优化
- module: 每一个文件其实都可以看成一个 module
- chunk: webpack 打包最终生成的代码块,代码块会生成文件,一个文件对应一个 chunk
- 可选值
- natural 按使用顺序的数字 ID
- named 方便调试的高可读性 id
- deterministic 根据模块名称生成简短的 hash 值 默认
- size 根据模块大小生成的数字 id
- 使用
- optimization.moduleIds
- optimization.chunkIds
-
模块联邦
- umi 最新的加速编译模块 mfsu 用到了这个技术
- 使用 Module Federation 时,每个应用块都是一个独立的构建,这些构建都将编译为容器
- 容器可以被其他应用或者其他容器应用
- 一个被引用的容器被称为 remote, 引用者被称为 host,remote 暴露模块给 host, host 则可以使用这些暴露的模块,这些模块被成为 remote 模块
- 参数配置
- name 必传值,即输出的模块名,被远程引用时路径为{expose}
- library 声明全局变量的方式,name 为 umd 的 name
- filename 构建输出的文件名
- remotes 远程引用的应用名及其别名的映射,使用时以 key 值作为 name
- exposes 被远程引用时可暴露的资源路径及其别名
- shared 与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖 公共作用域概念 使用方和被使用方的模块会统一放到一个池子里 供双方使用
-
使用内置插件 const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
-
需要结合 react.lazy 和 React.Suspense 异步加载来结合使用