为什么 vue/vite 源码以及生态仓库要迁移 pnpm?

前端三元同学

共 6864字,需浏览 14分钟

 ·

2021-12-11 16:13

前言

随着前段时间尤大在 vue3 以及 vite 仓库中切换包管理为 pnpm 的 pr 成功 merge,以及 vue 生态中的一些项目例如 VueUse 也切换使用 pnpm,宣告着 vue 生态中项目仓库完成了从原有的 yarn workspace monorepo 到 pnpm workspace monorepo 的迁移。

可以看到 vite 核心贡献者以及 vue 团队成员之一的 patak (https://github.com/patak-js) 在 twitter 上对这次项目迁移的生动描述:“项目如同多米诺骨牌一样倒向了 pnpm”。

具体关于 pnpm 相关介绍可以参考笔者之前写的一篇文章: pnpm: 最先进的包管理工具 ,本文中不会对此做更多的介绍。

vue 迁移项目

其中关于 vue 生态中项目迁移的具体过程可以参考这些的一些 pr:

  • https://github.com/vuejs/vue-next/pull/4766

  • https://github.com/vitejs/vite/pull/5060

  • https://github.com/vueuse/vueuse/commit/826351ba1d9c514e34426c85f3d69fb9875c7dd9

其中包括目前 vue3.0 项目源码仓库:

以及目前社区里面火热的 bundleless 工具 vite 源码仓库:

可以看到这两个的迁移 pr 都是由尤大亲手完成改造,同时 pnpm 作者的本人 zkochan(github: https://github.com/zkochan) 也亲自帮 vite 迁移的 pr 做了 code review

以上几个项目都是基于 monorepo 来做的仓库管理,pnpm 的 workspace 在 monorepo 场景下是有着极好的支持,当然也有非 monorepo 项目的迁移,例如由笔者迁移的 naive-ui 仓库的项目中包管理工具为 pnpm 用于提升 CI 下依赖安装速度的提升(参考pr: https://github.com/TuSimple/naive-ui/pull/1425 )。

下面来介绍一些这次的迁移动机以及引发问题的源头,注意以下的内容都是根据一些社区讨论进行推断的,可能并不完整或者准确,但是对于具体的细节我会尽量还原到位,同时也不会对此过度解读,希望读者自行甄别。如果错误,欢迎指正。

迁移原因

在尤大9月份的一条 twitter 中,发起了一条关于包管理器的投票,当时笔者正混迹 pnpm 社区,对此也有所耳闻,具体的投票结果可以参考:

pnpm 作者本人 zkochan 对此结果还是很满意的,因为之前统计社区的一些趋势,pnpm 并没有达到过如此高的使用率。

随着该条 twitter 之后,尤大又更新了一条 twitter (这里直接贴原文的内容):

esbuild 0.13 now uses optionalDependencies to install platform-specific binaries. Yarn 1/2 will download all binaries before picking the right one. Other (update to date) package managers only downloads the matching one.

This may be the thing that pushes me away from Yarn 1 :/

翻译过来的内容大概是 esbuild 在 v0.13 之后使用了optionalDependencies 来安装某些不同平台的依赖(相关 pr 可以参考: https://github.com/evanw/esbuild/pull/1621)。但  yarn 1/2 并不会根据对应的 optional 规则去下载对应平台的包而是会去选择下载所有的包。

那么为什么 esbuild 的一个调整会对尤大产生这样的念头呢,因为 vite 目前会在一些场景下使用到 esbuild 这个库:例如目前开发阶段 vite 会使用 esbuild 进行依赖预打包,来将第三方依赖转成 ESM 格式的 bundle 产物。

这样的关系使得 esbuild 作为了 vite 的一个底层依赖,前面也提到过 vite 本身仓库是基于 yarn workspace monorepo 搭建的,因此每次在开发 vite 时使用 yarn 安装依赖的过程中,都会去安装 esbuild 以及相关的包。

下面笔者会详细介绍一下 esbuild 的这个改动的原理以及为什么这个改动会使得 vite 将原有的 monorepo 架子直接做了迁移。

依赖分发机制

在上一节中提到了 esbuild 使用 optionalDependenceis 来作为目前的依赖安装策略,这节来介绍一下像这样这些跨平台的包依赖分发的过程。

其实关于这部分,具体可以参考社区中这篇: 用 Rust 和 N-API 开发高性能 Node.js 扩展(文章地址: https://zhuanlan.zhihu.com/p/234914336) 文章最开头的一部分内容。

这里笔者以 nodejs 原生拓展(native addon)的代码分发方式为例子做个介绍:

关于 nodejs 拓展开发可以参考笔者之前写过的一篇文章: Nodejs 的 C++ 拓展开发

其中主流的分发方式大概有这样两种:

分发 JS 代码,postinstall 去下载对应产物

一般使用其他语言开发的 addon 之类的会把产物打包成一个可执行的二进制文件(例如 C++ 拓展一般是 .node 结尾的文件)。

postinstall 脚本安装的方式其实在社区中也是比较常见的,例如安装 node-sass 就会按照这样的模式进行:

node-sass 会把 native addon(C++ 开发) 的预编译产物放在一个 CDN 地址里面,然后用户在使用 npm install 安装 node-sass 的时候,会通过 postinstall 脚本将 addon 产物文件从 CDN 上下载下来。

包括 v0.13 版本之前的 esbuild 其实也是采用这种方式来进行分发。

这种方式其实有个缺点,可以看到图中下载的二进制文件地址是个 Github release 地址,这种情况下常常会因为无法兼顾国内/海外用户。不过一般可以通过在国内搭建一个相关的下载镜像来解决这个问题,但镜像不同步的问题也是时常会发生的。

不同平台的 native addon 通过不同的 npm 包去分发

目前市面上很火的两个构建工具,swc 和 esbuild 就采用的这种方式。每一个 native addon 对应一个 npm 包。然后将所有的 native addon 对应的 npm package 作为 optionalDependencies, 并在这些 npm package 的 package.json 中的 os 以及 cpu 字段,让对应的包管理工具在安装的时候对不同平台的包自动选择去安装哪个 native package,例如 esbuild 目前的 npm 包结构:

{
"name": "esbuild",
"version": "0.14.1",
"optionalDependencies": {
"esbuild-android-arm64": "0.14.1",
"esbuild-darwin-64": "0.14.1",
"esbuild-darwin-arm64": "0.14.1",
"esbuild-freebsd-64": "0.14.1",
"esbuild-freebsd-arm64": "0.14.1",
"esbuild-linux-32": "0.14.1",
"esbuild-linux-64": "0.14.1",
"esbuild-linux-arm": "0.14.1",
"esbuild-linux-arm64": "0.14.1",
"esbuild-linux-mips64le": "0.14.1",
"esbuild-linux-ppc64le": "0.14.1",
"esbuild-netbsd-64": "0.14.1",
"esbuild-openbsd-64": "0.14.1",
"esbuild-sunos-64": "0.14.1",
"esbuild-windows-32": "0.14.1",
"esbuild-windows-64": "0.14.1",
"esbuild-windows-arm64": "0.14.1"
}
}

例如其中对应的 esbuild-android-arm64 一个安卓平台的包,以及 arm64 架构的包的 package.json 内容为:

{
"name": "esbuild-android-arm64",
"version": "0.14.1",
"os": ["android"],
"cpu": ["arm64"]
}

这种方式可以认为是目前对使用 native addon 用户影响最小的分发方式,包括这里提到的 esbuildswc 以及 napi-rs 都是采用的这种方式。

这种方式存在的缺点可能就是对开发者的负担会比较大:因为需要同时维护多个系统以及 CPU 架构的包。同时开发/调试也需要消耗很大的工作量。

前面提到的 vite 底层的依赖项 esbuild 在 v0.13 之后由 postinstall script 安装的方式迁移到了这种 optionalDependencies 的方式:

包管理器支持

在上一节中我们介绍到了 native addon 的一些常见的依赖分发机制,同时也介绍到了 esbuild 目前在 v0.13 之后采用了 optionalDependencies 机制。前面也有提到因为 yarn1 的依赖安装机制问题导致在 vite 进行开发时,每次都会下载 esbuild 中所有的跨平台包(例如在 android 平台上也会下载 ios 的包),对此会导致每次给 vite 仓库进行依赖安装的时候,耗费很久的时间。参考 yarn 下面的 issue(https://github.com/yarnpkg/berry/issues/3317)。

举个例子来说(该例子来自于 esbuild 作者 evanw 解释):

以目前 pnpm v6.14 的行为来说,对于 esbuild 下的 optionalDep 进行依赖安装的时候,下面有各种跨平台以及 cpu 架构的包,但实际上只会对符合当前平台架构的包进行实际的依赖安装,其他非这一类的包只是会生成一个 meta data 的数据在 lock 文件上。

实际上包的体积时远远大于这份数据的大小(例如数据约 0.5MB,一个包大约 8MB),那么假设 optionalDep 下面存在 13 个 package,那么 pnpm 大概会安装约 0.5mb * 13 + 8mb = 14.5mb 体积的包,而 yarnv1 则会安装约 0.5mb * 12 + 8mb * 12 = 102mb 的包,这样会使得因为包管理工具不同的情况下,yarn 安装的东西比 pnpm 远多,从而导致依赖安装的时间会很慢。

关于上面提到的依赖安装问题可以参考下表中的 Downloads extra data 这一栏,可以看到目前 yarn 只有 yarnv3.1.0 这个版本修复了该问题,pnpmnpm v7 以及 cnpm@7.1.0 都解决了这个该问题。

vue 生态项目完成迁移

尤大在社区里面参考了一些开发者的意见以及发起了一个关于包管理器的投票,twitter 下 90% 左右的回复都推荐了 pnpm,包括目前 vue core team 的 antfu(https://github.com/antfu) 也已经在自己的开源项目 slidev(https://github.com/slidevjs/slidev) 中实践使用了 pnpm,同时也对 pnpm 的一些功能赞不绝口。

于是 vite 直接在几天之后开始了由 yarn workspace 到 pnpm workspace 的迁移:

在迁移过程中虽然遇到了一些问题,但基本上随着 pnpm 作者以及社区的帮助努力下,最后也都成功完成了,实际上的迁移成本也没有特别的大,可以参考前面的 pr。

在 vite 完成迁移之后,其他的 vue 生态项目也紧随其后,虽然这些其他的项目底层可能没有像 vite 遇到的 esbuild 的问题那样,但 pnpm 的一些其他优势(例如对依赖的严格管理,快速的依赖安装,天然的 monoreo workspace 支持等)也吸引着 vue 生态迁移了包管理工具。因为有了 vite 迁移的经验,其他项目的迁移也都很快完成了,基本上 vue3 的一个迁移相关的 mr 在一天的时间内就完成了合并,慢慢地几乎 vue 生态里面大部分项目都完成了迁移。

迁移 pnpm 的实践

如果想了解如何从一个完整的 yarn workspace 项目迁移到 pnpm workspace,其实也不用去专门研究 vite 或者 vue3 的 pr 是怎么迁移的,在 pnpm 官网上有一篇来自于社区的文章: Replacing Lerna + Yarn with PNPM Workspaces (地址: https://www.raulmelo.dev/blog/replacing-lerna-and-yarn-with-pnpm-workspaces)。

作者算是比较详细的介绍了如果从yarn workspace(项目基于 lerna,但区别其实不大),迁移到 pnpm workspace 需要做的文件改动以及项目变更。大概是这样的一个流程:

  • 替换掉脚本命令,与 yarn 相关的命令替换为: pnpm  或者 pnpm run

  • 删除掉顶部 package.json 中的 yarn workspace 配置

  • 替换掉的 workspace 配置用 pnpm-workspace.yaml 文件替代

  • 调整 pipeline、以及 Dockfile 或者其他 CI/CD 配置文件里面的依赖安装命令

  • 删除掉 yarn.lock 文件(这里也可以使用笔者开发完善的 pnpm import 命令来完成 yarn.lock 文件转换 /笑 )

  • 调整构建相关的脚本(如果有 lerna 相关的 build 脚本)

  • 添加一个 .npmrc 文件用于自定义一些 pnpm 的 CLI 行为表现(也可以不用)

感兴趣的同学可以去参考一下,或者直接和笔者进行交流也可以(笔者在字节也迁移过比较多这一类型的项目,对此也有一些经验,这里就不做过多的介绍了)。

总结

其实之前在尤大发起关于包管理工具的投票时,笔者就已经注意到了,同时也关注到了 vue 开始了 pnpm 的迁移,但当时并没有去仔细关注底层的原因。

之前 yarn 的作者发布了 yarnv3.1(文档见: https://dev.to/arcanis/yarn-31-corepack-esm-pnpm-optional-packages--3hak),  里面最吸引人注意的 feature  可能是: yarn 在这个版本下支持了 pnpm 模式的依赖安装方式(即 content-addressable store),但 yarn 的作者表示这次版本发布中实现的最复杂的 feature 是支持了本文中提到的按需安装不同平台以及 cpu 架构的依赖包:

笔者在最近学习 swc 的时候,注意到了这一点,抱着刨根问底的心态,去研究了一下这一系列迁移问题背后的原因(原因令人暖心)。

最后写了这样一篇干货性不是很强的文章,可以作为一个记录如果之后有相关的需求进行开发(例如使用其他语言开发 native addon 的时候)的话。同时也希望 pnpm 未来能成为一个社区中流行的包管理工具吧~

浏览 55
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报