2024 webpack 面试题 学习笔记

399 阅读10分钟

Webpack 构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

简单说:

1 初始化 init:读取与合并配置参数,实例化 Plugin,实例化 Compiler

2 构建阶段 make:从 Entry 入口开始,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理,并根据模块之间的依赖关系构建 ModuleGraph

3 生成阶段 seal:根据 ModuleGraph 构建 ChunkGraph,开始遍历 ChunkGraph,转译每一个模块代码,将 Chunk 转换成文件,将所有模块与模块运行时依赖合并为最终输出的 Bundle

细化流程:

Loader Plugin 区别

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 Loader 成了翻译官,对其他类型的资源进行转译的预处理工作。

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件钩子,Plugin可以监听这些API接口,在合适的时机改变输出结果。

source map

Sourcemap 是一种高效的位置映射算法,它将编译、打包、压缩后的代码映射回源代码之间的位置关系表达为 mappings 分层设计与 VLQ 编码,再通过 Chrome 异地还原为接近开发状态的源码形式

map文件只要不打开开发者工具,浏览器是不会加载的

线上环境暴露源码有安全风险,一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用
  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
  • sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline- 和 eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

模块打包原理

webpack通过__webpack_require__ 函数模拟模块的加载(类似于node中的require语法),把定义的模块内容挂载到module.exports上。

webpack 模块加载方式都是同步方式
webpack 有个 require.ensure api语法来标记为 异步加载 模块

watch 文件监听

原理:轮询判断文件的最后编辑时间是否变化

如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。

在发现源码发生变化时,自动重新构建出新的输出文件。

Webpack 开启监听模式:

启动 webpack 命令时,带上 --watch 参数

在配置 webpack.config.js 中设置 watch: true

缺点:每次需要手动刷新浏览器

module.export = {
  // 默认false,也就是不开启
  watch: true,
  // 只有开启监听模式时,watchOptions才有意义
  watchOptions: {
    // 默认为空,不监听的文件或者文件夹,支持正则匹配
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行,默认300ms 
    aggregateTimeout:300,
    // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
    poll:1000
  }
}

HMR 热更新

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

0 开发服务与浏览器之间维护了一个 Websocket

1 当开发环境的资源发生变化时,dev-server 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比

2 客户端对比出差异后会向 dev-server 发起 Ajax 请求来获取更改内容(文件列表、hash)

这样客户端就可以再借助这些信息继续向本地服务器发起 jsonp请求获取该chunk的增量更新。

hash 文件指纹

文件指纹是打包后输出的文件名的后缀:

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的hash值就会更改
  • Chunkhash:和Webpack打包的chunk有关,不同的entry会生出不同的chunkhash
  • Contenthash:根据文件内容来定义hash,文件内容不变,则contenthash不变
module.exports = {
  output: {
    filename: '[name][hash:8].js',
    // filename: '[name][chunkhash:8].js',
    // filename: `[name][contenthash:8].css`
    path:__dirname + '/dist'
  }
}

如何优化构建速度

1 使用高版本的 Webpack5 和 Node.js

2 缩小打包作用域 (exclude/include、resolve.extensions、alias)

3 多子进程构建:thread-loader

4 SplitChunksPlugin进行分包(让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间)

5 充分利用缓存提升二次构建速度:cache-loader

6 动态Polyfill(建议采用 polyfill-service 只给用户返回需要的polyfill)

thead-loader

多子进程打包:某个任务消耗时间较长会卡顿,多进程可以同一时间干多件事,效率更高。babel-loader消耗时间最久,所以使用thread-loader针对其进行优化

优点:提升打包速度
缺点:每个进程的开启和通信都会有额外的开销,当项目较小时,使用多进程打包反而造成打包时间延长

thread-loader写在最后执行,因为loader是从下往上,从右往左的执行顺序

thread-loader之后的loader会在一个独立的worker池中运行

thread-loader进程启动大概为600ms,进程通信也有开销

编写 loader 的思路

Loader 是链式调用,会按照use定义的顺序从前到后预处理执行Pitch Loader,从后到前翻译执行Normal Loader流程,这个过程中,Loader Context提供了许多实用接口,可以借助这些接口读取上下文信息,

来进行对UTF-8格式的字符串的处理

使用 schema-utils 校验配置参数是否合法

使用 loader-utils 拼接产物路径

编写 Plugin 的思路

1 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例。

2 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象。

3 插件实例在获取到 compiler 对象后,就通过compiler.plugin(事件名称, 回调函数) 监听到Webpack广播出来的事件。并且可以通过compiler对象去操作chunk代码。

Babel 编译

Babel大概分为三大部分:

  • 解析:将代码转换成 AST
    • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
    • 语法分析:分析token流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST
  • 生成:以新的 AST 为基础生成代码

JS 编译原理

JS编译分三个步骤,词法分析、语法分析以及代码生成

编译过程涉及三个角色: 引擎、编译器和作用域

引擎是贯穿整个编译过程的,相当于主干。而编译器,主要是负责词法分析、语法分析与代码生成。作用域主要负责收集与维护标识符集合(应该就是我们声明的变量),并且控制当前代码对标识符的访问权限。先来看看词法分析、语法分析、代码生成的过程。

Tapable 流程管理

tapable 是 webpack 内部使用的一个流程管理工具,主要用来串联插件,完善事件流执行

Tapable 是一个用于事件发布订阅执行的插件架构,注册的回调在触发时按顺序执行。

通过 arguments 获得所有传入的插件对象,并调用插件对象的apply方法,注册插件(所以,一个合法的插件应该包含入口方法apply)

使用方法主要是三个步骤(以同步钩子为例)

  1. new SyncHook(['xx']) 实例化 Hook
  2. hook.tap('xxx', () => {}) 注册钩子
  3. hook.call(args) 触发钩子

为了应对构建场景下各种复杂需求,Webpack 内部使用了多种类型的 Hook,分别用于实现同步、异步、熔断、串行、并行的流程逻辑,开发插件时需要注意识别 Hook 类型,据此做出正确的调用与交互逻辑。

Dependency Graph

Dependency Graph 模块之间隐式形成了以 entry 入口模块为起点,其他模块为节点,以导入导出依赖关系为边 的依赖关系图。Dependency Graph 是 Webpack 底层最关键的模块地图数据。

Webpack 构建过程中,从 entry 模块开始,会持续收集模块之间的引用、被引用关系,逐步递归找出所有依赖文件,并记录到 Dependency Graph 结构中,后续的 Chunk 封装、Code Split、Tree-Shaking 等,但凡需要分析模块关系的功能都强依赖于 Dependency Graph。

Chunk Graph

「构建」阶段负责根据模块的引用关系构建 ModuleGraph;「封装」阶段则负责根据 ModuleGraph 构建一系列 Chunk 对象,并将 Chunk 之间的依赖关系(异步引用、Runtime)组织为 ChunkGraph —— Chunk 依赖关系图对象。与 ModuleGraph 类似,ChunkGraph 结构的引入也能解耦 Chunk 之间依赖关系的管理逻辑,整体架构逻辑更合理更容易扩展。

「封装」阶段最重要的目标还是在于:确定有多少个 Chunk,以及每一个 Chunk 中包含哪些 Module —— 这些才是真正影响最终打包结果的关键因素。

ChunkGraph 构建流程最终会将 Module 组织成三种不同类型的 Chunk:

  • Entry Chunk:同一个 entry 下触达到的模块组织成一个 Chunk;
  • Async Chunk:异步模块单独组织为一个 Chunk;
  • Runtime Chunk:entry.runtime 不为空时,会将运行时模块单独组织成一个 Chunk。

Tree-Shaking

一是构建阶段需要先 「标记」 出模块导出值中哪些没有被用过;二是生成阶段使用代码压缩插件 —— 如 Terser 删掉这些没被用到的导出变量。

Tree-Shaking 是一种只对 ESM 有效的删除技术,它能够自动删除无效(没有被使用,且没有副作用)的模块导出变量,优化产物体积。不过,受限于 JavaScript 语言灵活性所带来的高度动态特性,Tree-Shaking 并不能完美删除所有无效的模块导出,需要我们在业务代码中遵循若干最佳实践规则,帮助 Tree-Shaking 更好地运行。

webpack5 新特性

1 缓存module来提高构建性能: 每当代码变化、模块之间依赖关系改变导致graph改变时,Webpack会读取记录做增量编译(缓存 module objects)

2 moduleIds & chunkIds的优化: 打包名可以固定关联name,来改进长期缓存 (named)

3 更智能的tree shaking:清除未使用的导出和混淆导出,来缩小打包体积

4 移除nodeJs的polyfill脚本(polyfill可以帮助编译,但是线上代码并不需要)

5 按模块大小进行SplitChunk拆分(maxSize/minSize)

6 MF模块联邦:让Webpack打包的达到了线上runtime的效果,让代码直接在独立应用间利用CDN直接共享

Webpack vite 区别

Vite 的主要功能就是通过劫持浏览器的script module请求,将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack开发编译速度快出许多, 常用在应用开发阶段,生产阶段用 rollup

1、启动速度: webpack服务器比vite慢; 由于vite启动的时候不需要打包,也就无需分析模块依赖、编译,所以启动速度非常快。

2、热更新: vite比webpack快;vite在HRM方面,当某个模块内容改变时,让浏览器去重新请求该模块即可。3、vite用esbuild预构建依赖,而webpack基于node。

4、生态: vite的不及webpack,加载器、插件不够丰富。

manifest 文件

manifest 就是记录模块依赖关系的JSON文件

当浏览器通过runtime加载模块包时,就会先根据manifest文件来查找模块的依赖关系,在按需加载模块