Huozhi

制作一个简单的 JS/TS 的零配置打包工具

bunchee

大约 18 年下旬想要搞一个方便的打包工具,面向的人群是非前端领域,对 JS 没有那么熟悉的做其他方向的 developer 和 research developer。彼时从其他方向 dev 们那听到最多的声音就是关于对各种 JS 语法兼容、语言特性、模块化的疑惑,不像 Java 有很多完整的标准和限制,在工程实践的时候不会有太多不确定的选项。

另一方面,自己组维护的一个很大的核心 library,虽然一直是用 ES6 作为基础语法,但是里面还是掺杂了许多 ES5 甚至跟老的语法。它下面依赖了一些其他组提供的 npm 包,有的是 UMD 有的是 CommonJS;有的全部 dependency 都打包进 dist,有的只打包了自己的 source。在这样百花齐放的情况下我决定做一些简单的 align,让大家尽量使用同一套工具链,减少在这方面花费的经历,让 researcher dev / dev 专注在自己实现的算法和功能上。

Pick 一套通用的语法集

除了某些很古老的文件还是用了 ES3,大家大多数都在使用 ES6,没有使用到 ES6+ 更多的未完全从 stage x 走到稳定的语法,比如 decorator(因为都不是 react UI 的 lib,基本上都自己写 bind 了)。为减少语言上的心智负担,我采取了下面的措施:

写了一些简单的 codeshift 的脚本,把冗长的老 code 先转化成 ES6(比如用 function 实现 class,用 defineProperties 定义 getter / setter 的 code 都转化成 ES6 class + getter / setter) 限制只使用 ES 在 tc39 进入稳定阶段的语法。因为其实现阶段对我们完全足够,很多算法功能或者实现一些其他非 UI 相关功能的库并用不上太多 fancy 的语法。让非 JS dev(如 research 的,做数据的)可以专注他们要想的事情。 依旧只支持 JS,因为他们没有使用 TS 的打算。Lea 的这篇文章 更加坚定了我不给其他 dev 提供过多复杂选项的想法

选取制作打包工具的库

由于是 browser runtime 的 library 和 npm package 为主,不太存在需要单文件(无依赖)执行或者需要打包成单文件在浏览器跑的 JS。

打包(Bundler): 决定选用 rollup,剩下的集成和到底打包成什么其他格式,留给应用层的 webpack 去思考。 编译(Compiler):这个比较有意思,开始选用了 babel v7,但是想来想去如果有人装了 v6 的 babel-core 在外面或者自己配了其他 babel plugin 产生了冲突和 side effects,那么编译出来的东西还是不太好控制。开始我发现了 bublejs ,一个轻量级的 ES6 到 ES5 的编译器,底层也是 acornjs,但支持的显然不是 babel 那么灵活和广泛。 再后来 babel 到 v7 后,由于 buble 本身的一些限制(比如现在还没支持 generator),还是决定切换到 babel。然后又加上了 ts 的支持,让他也可以打包 ts,这样也算解决了很多喜欢使用 ts 语言朋友的问题了。

所以最终的工具链是:rollup + babel (presets + plugins) + ts

如何使用

使用方面借鉴了 developit/microbundle 的使用,可以在 package.json 里指定各种 format 的dist file 路径,如 "mian": "dist/bundle.js" 然后用 bunchee 的 CLI 去编译源文件入口就行了。e.g.

{
  "main": "dist/pkg.cjs.js",
  "module": "dist/pkg.esm.js",
  "scripts": {
    "build": "bunchee ./src/index.js"
  }
}

但是我只支持在 main 和 module 两个字段,即只支持 ESModule 和 CommonJS。原因是我觉得既然是 library,做成 ESModule 是最有利于 treeshaking 等优化,以及最后的输出形式应该还是在 application 那块控制。module 和 main 是 webpack 可以认的 main fields,其实也可以支持 "browser" field,但是我觉得不应该所以没有支持。browser field 太灵活了,也不知道你文件到底是什么 format,umd/amd/cjs... 我选择迎合了新标准 ESModule,也给提供 CommonJS 的机会。

@尤雨溪 老师最近在做 vite 的过程中应该也遇到了类似的问题,package.json 里 browser field 指向了一个 cjs format 的文件。因为 webpack 能支持的太多了,我们的使用就变得越加地随意

当然也支持你使用 bunchee CLI 直接指定输出形式,这里我选择支持了 UMD,可以和 rollup CLI 一样使用 bunchee --umd -o dist.umd.js 来输出。原因是手动执行有更多的灵活性,并不和 package.json 中的 main fields 字段绑定,你可以拿来做其他的比如在浏览器中运行的测试。

打包出来的文件不需要压缩,如果有压缩混淆需求,还是交给集成 npm package 的 app 来处理。

实现其实很简单,感兴趣的可以看一下 github repo huozhi/bunchee 或者直接用 npm 上下载使用,就不过多介绍。

开源相似品对比

开源的其实也有很多 zero-config bundler,比如 parcel 面向的是 application,还有 microbundle 面向的是 library,还有 vercel 的 ncc。

microbundle

排除面向 app 的,我觉得没有使用 microbundle 的原因还是因为它做的事情太多了,依赖也很重。里面有 buble 又有 babel,还有 typescript 的支持,甚至默认使用了 terser 做压缩,还有一些给 package.json 加的字段如 source, esmodule, etc. 虽然只是 dev dependecy,但我还是觉得太“重”了,在 zero 的基础上又给了外面用户各种配置的选项,有点不够 zero。当然我很喜欢它,确实十分便利。

ncc

ncc 会更加复杂一些,而且 ncc 使用了 webpack 且主要面向 nodejs,我不是很喜欢 webpack 编译出来的语法,感觉为了处理各种 module 的情况做了很多兼容,而且 webpack 很重。对于一般的小 lib,ESModule 完全足够,只支持它也是希望限制大家随意使用包格式。

我个人比较崇尚 minimalist。像做菜使用最灵活的小刀完成各种切割任务,大菜刀对我来说还是有点麻烦,所以还是选择自己做了。

自举

在看给 @vercel/ncc 贡献代码的时候无意发现 ncc 是可以自己编译自己的,就想到是不是应该也可以让 bunchee 自己打包自己。最后经过一番折腾,将 bunchee 分成了 bundle core 和 CLI 两部分,CLI 会调用 bundle core 来实现打包,最终完成了自举。最新的 bunchee 也是用上一个版本的 bunchee 自己打包发布的。

大概就是这样。

© 2020, Huozhi