Build your own Bunlder

Tags
Web Dev
Bundler
Published
September 25, 2021
Author
HJS
在传统上,由于浏览器的并发请求数量限制,需要将多个 JavaScript 模块打包到一个文件中,即使在 ESM 和 HTTP/2 得到广泛支持的今天,构建工具仍然发挥着重要作用,其中又以 Webpack 的生态最为繁荣,支持的功能最多
抱着学习的目的,让我们试着实现一个 bundler,以下是我感兴趣的部分:
  • 分析并打包多个 JS 模块到一个 bundle 中
PS. 代码存在大量简化处理,仅为理解大致的实现原理,不能用于生产环境😅
先给出源代码,是一个 monorepo 仓库,包含 application,mypack,mypack-cli,mypack-dev-server 四个 package。下面给出大体的实现思路,细节不过多赘述。
 

打包文件

看一下源文件用 webpack 的打包后的结果
notion image
notion image
小结:
  • 模块的组织方式为以当前文件的路径为 key,内容为 value 的对象
  • import 路径被替换成以 src 为根目录的相对路径
  • 语法被解析器转换为与目标环境兼容的语法
 
关键代码:
  1. 基于解析器做模块路径的分析和代码转换 —— L26-L28
  1. 递归读取、处理文件,构造模块的依赖图 —— L73
  1. 将构建的 initial chunk 写入文件
 

动态导入

上图中可以看到异步导入被转换为 Promise 的形式,但在 ESM 被广泛支持的今天,我不打算对这部分代码进行转换。实际效果:
notion image
 
关键代码:
  1. 基于解析器做动态导入模块路径的分析 —— L30-L38
  1. 递归读取、处理文件,构造动态导入模块的依赖图 —— L87
  1. 将构建的 non-initial chunk 写入单独的文件
Ps. 异步组件的渲染时由前端框架进行处理,和构建工具无关。
 

模块热替换(HMR)

模块热替换的细节处理逻辑很多,但大体思路上还是很清晰的,替换掉维护在 __webpack_modules__ 的模块,触发 module.hot.accept 的事件。
 
关键代码:
  1. 模块热替换需要注入 runtime,包含事件模型、WebSockets 等 —— L117-L204
  1. 基于 chokidar 监听代码变动,重新构建模块,写入 hot-update.js。如果更新的文件是在异步 chunk 中,则通过 WebSockets 通知客户端 reload —— L26-L46
  1. Dev server 通过 WebSockets 通知 bundler runtime 文件更新,如果是 Webpack 此时还会对比文件的 hash 是否不同,此处简单处理,直接动态加载脚本更新模块 —— L157-L175
  1. 触发 HMR 的事件执行,为了获取最新的模块内容,需要注入重新获取模块的代码,并且需要将作用域内与模块关联的所有引用提升为全局变量,这里我们写一个 babel plugin 去做这件事 —— plugin-transform-module
 

小结

上文给出了简单 bundler 的实现思路,细节上构建工具要处理的的 case 非常多 🙈,例如分析模块路径时就可能是:
  • 文件相对路径
  • node_modules 中的 library
  • bundler 配置中的 alias
Webpack 光解析路径就写了 enhance-resolve 库,因此实现一个完整构建工具的 Effort 还是非常庞大的,那么仅了解一些实现原理对我们有什么用呢?举一个作者本人的例子:
业务中使用了 Webpack 的 Module Federation 这一特性,但目前 MF 仅能提升跨团队独立开发的效率,开发完却不能支持子应用热部署。通过我们上文的分析可知,Webpack 的模块都是维护在一个全局变量中,我们只需要在子应用更新时,通知宿主应用移除掉对应的模块,当重新渲染该模块时,webpack runtime 即可加载最新的远程代码。