随着时间的推进,是时候翻新我们的 js / ts 工具链的时候了,对此我们进行了一些探索。
Volta 是最近在 github 上发现的 node 版本管理工具,基于 rust 开发,速度非常快,整体功能和 nvm 非常接近。
基本上可看作是 nvm 的部分增强版。
在 Node 项目开发中,我们需要对依赖进行 bundle 操作,最近我们将评估几种 bundle 工具。主要有 @vercel/ncc, swc, tsup 和 bun。
其中 @vercel/ncc 在我们的项目中已经使用了 2 年,只不过最近一年中 ncc 的开发近乎于停滞阶段,所以我们需要对比一下其他的工具。
经过评估之后,我大体上总结了一下这几个工具的特点:
@vercel/ncc | swc | tsup | bun | |
---|---|---|---|---|
开发语言 | JavaScript | Rust | TypeScript | Zig, C++ |
底层 | webpack | esbuild / rollup | ||
速度 | 慢 | 极快 | 快 | 极快 |
bundle 功能 | ✅ | ☑️ (实验性) | ✅ | ✅ |
bundle 范围 | 项目代码, 依赖, assets, napi | 项目代码,依赖 | 项目代码, 依赖 | 项目代码,依赖 |
项目活跃度 | ⭐️⭐️(目前停滞) | ⭐️⭐️⭐️⭐️⭐️ (bundle 功能 ⭐️) | ⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ |
github stars | ~9k | ~30k | ~7.9k | ~69k |
总结:
esbuild
的生态较为完善,本身基于 Go 语言开发,运行速度较快,tsup 不但速度和功能上都有保障,不过原生不支持 bundle assets,需要结合 esbuild 插件和相关代码调整来实现目前这几个工具都能正确处理依赖打包问题,不过需要注意的是部分工具需要正确配置,例如 tsup
需要将要打包的依赖写入 devDependencies
,对于 dependencies
和 peerDependencies
的依赖是不会 bundle 的。
这个功能不得不提,在 bundle 非常重要,需要在构建的时候将使用的资源文件一并打包,并且在产品环境中可以正确调用。
在 ncc 中这个功能是开箱即用的,具体参见 Asset Relocation 文档,了解它是如何工作的。
我们先来看一个范例,假设我们的项目中包含一个图片,我们的代码中需要使用这个图片,那么我们可以这样写:
const pngFile = path.join(__dirname, "assets", "sammy.png");
console.log("png file path is: ", pngFile);
console.log("is this file exists ? " + fs.existsSync(pngFile));
在开发环境中运行没有任何问题:
$ npx -y tsx index.ts
png file path is: ***/assets/sammy.png
is this file exists ? true
于是使用 ncc 编译一下看看:
$ npx ncc build index.ts
ncc: Version 0.38.1
ncc: Compiling file index.js into CJS
ncc: Using typescript@5.4.3 (local user-provided)
4kB dist/index.js
117kB dist/sammy.png
121kB [970ms] - ncc 0.38.1
NOTE: 想要使这个效果生效,必须将
tsconfig.json
中的module
设置为ESNext
,不能使用CommonJS
,否则这个打包是不起作用的,参见 issue: vercel/ncc#829
从输出中我们就可以看到 ncc 已经将图片资源一并打包进去了,并且代码中还正确处理了 path.join
的路径,这样我们在生产环境中就可以正确的调用这个图片了:
$ node dist/index.js
png file path is: ***/dist/sammy.png
is this file exists ? true
那么用 tsup
看一下呢?
❯ npx -y tsup index.ts --clean
CLI Building entry: index.ts
CLI Using tsconfig: tsconfig.json
CLI tsup v8.0.2
CLI Target: es2016
CLI Cleaning output folder
CJS Build start
CJS dist/index.js 1.47 KB
CJS ⚡️ Build success in 12ms
仅从输出中就可以看出来图片并没有一并打包到 dist/
目录下,更别提 path.join
的路径处理了,果然运行有问题:
$ node dist/index.js
png file path is: ***/dist/assets/sammy.png
is this file exists ? false
类似的,其他两个工具也不能处理这种情况。
但是由于 tsup 支持 esbuild 插件,因此我们可以通过 esbuild 插件解决这个问题,比如这个 esbuild-plugin-copy 以及 esbuild-plugin-raw,或者指定 --publicDir
参数来解决 copy assets 问题,但是特别需要处理路径问题,他们是不会帮你去处理相对路径的问题的,使用者必须自己注意这些路径的变化,选择合适的路径保持兼容。
特别要注意 tsup 还提供一种方式解决这个问题,那就是使用 import static assets
,参见 issue egoist/tsup#392
也就是使用类似于各种前端框架 (react/angular/svelte/vue 等) 的 import
的方式引入资源,例如:
import pngFile from "./assets/sammy.png";
但是这个语法不能直接在 ts 中使用,import 默认只能导入 node 支持的模块类型,想要导入文件资源需要使用 pattern ambient module
功能,参见 ambient-modules
定义一个 global.d.ts
文件:
declare module "*.png";
然后再调整下代码:
import pngFile from "./assets/sammy.png";
const pngPath = path.join(__dirname, pngFile);
console.log("png file path is: ", pngPath);
console.log("is this file exists ? " + fs.existsSync(pngPath));
之后设置 loader
参数再编译:
$ npx tsup index.ts --loader '.png=file'
CLI Building entry: index.ts
CLI Using tsconfig: tsconfig.json
CLI tsup v8.0.2
CLI Target: es2016
CJS Build start
CJS dist/index.js 1.56 KB
CJS dist/sammy-OT6F4VL7.png 116.87 KB
CJS ⚡️ Build success in 13ms
此时我们看到图片资源已经被正确打包进去了,而且路径也是正确的:
$ node dist/main.js
png file path is: ***/dist/sammy-OT6F4VL7.png
is this file exists ? true
NOTE: bun 内置也有个 file loader,默认就支持这种 import 的方式,但是它对于路径处理的方式是不同的,它会返回绝对路径,因此不需要 path.join 处理正确的路径,特别需要注意
Turbo-repo 是我们近期准备采用的工具,它是一个 monorepo 管理工具,可以帮助我们更好的管理我们的 monorepo 项目。
Turbo-repo 自带有 task 缓存功能,在重复执行某些 task 的时候可以加速。
而且 task 之间还可以进行依赖管理,可以很方便的进行项目内的 CI 编排。
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章