为什么 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
用户影响最小的分发方式,包括这里提到的 esbuild
、swc
以及 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
这个版本修复了该问题,pnpm
、npm 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 未来能成为一个社区中流行的包管理工具吧~