前言
直入正题。之所以要聊聊 pnpm 的技术选型临界点 Critical adoption ,得益于一段时间以来在 pnpm 上的丰富实践和赋能落地积累的经验。
讲道理从 BD douyin 和 infra 团队相关开发者开始在国内推行和宣传,介入 pnpm 社区之后我也是对 pnpm 开始抱着探索的态度。
当然经过一段时间的实践考究,发现 pnpm 只能是属于功大于过,下面我们简单聊聊。
正文
本文的主题是 pnpm monorepo 实践,但是聊之前,我们先捋一捋其他包管理器 npm / yarn ,事物都有两面性,存在即合理。
npm
对于老牌 npm 来说现状是很严峻的,一方面是 nodejs 决定以后不再自带 npm 这个 “不争气” 的工具自己出了 corepack ,当然这也引发了 npm 的疯狂迭代,到现在已经 v8
了,可能大部分人都还在 v6
,当然 v6
是自带依赖安装缓存的,可能效果没那么好,但也不差,另一方面就是 v7
的伤其实就是把 package-lock.json
给 breaking change 了,可能这也是很多人即使知道他知错悔改了,但是不去选择他的新版本的一个原因,毕竟同类竞品更加强大。
但是 npm 作为一个 nodejs 自带的工具,特别是现在 fe 圈入门门槛极低,做东西成本也低(Github 一年新增 1600 多 w 的 js 开发者,可想而知),轮子也多,所以实际上 fe 圈的小白和新手或者处于初心、初级领域的开发者很多,那么对于他们来说可能用用 npm 也就是正确的选择,毕竟也不是不能用,也没特别差,也不需要追求极致。
当然,在考虑到很多历史老项目也都是沿用的 npm 管理,npm 的地位绝对是历史霸主,当然现在已经霸气全无了,我们就是硬着头皮在用。
yarn
yarn 这个东西其实国内大家都停留在 v1.2
这个经典的版本,因为他确实在机制上、速度上、易用性上都吊打了 npm ,那么上手了的 js 开发者都会选择 yarn 去管理依赖。
再聊聊国外,yarn v1.2
是一个很老的工具,很多人可能都不知道已经出了 yarn v2
,其实 v3
也出了,那么 yarn 新版本干了什么呢?v2
其实就是在做依赖的压缩化(pnp),讲道理我的评价是这个东西好坏参半,因为压缩后 type 完全汇总到一起了,一个文件大几十 MB ,对 win 系的系统直接就是拒之门外了,完全无法索引明白,到了 v3
官方意识到不对,开始疯狂追赶 pnpm 学习别人的特性,比如现在也出了 pnpm 的软链机制。
虽然国外使用 yarn v2+
的群体比较多,但终究还是少数,而且也没有什么历史性的冲击,所以在这个版本上,我们只是外围观看者而已,还没有足够的诱惑让我们跳坑。
不过不管怎么说,yarn 的使用量依然是现在全球第一的王者,加上 yarn workspace 和 lerna 的联动,就单单在这一个层面上就称霸了全世界,你可以说他不好,但是他就是现在的 The champion ,我们要认可他。
pnpm
解决依赖问题
pnpm 这个东西一出现大家都说软链的好,其实软链也有问题,即使在非 monorepo 的单仓,为了实现严格的依赖版本区分,可能会通过依赖隔离提升到
node_modules/.pnpm/react@17.0.2_@socpe+dep-name@1.0.0
类似这种为了隔离具体版本冲突的形式中去,其实这就是一个坑,因为他破坏了原有的这种 resolve 机制,传统 resolve 是我们默认他是放在 node_modules/*
下的,可以说是 yarn 的幽灵依赖的思维,那你去 resolve 还是 enhancer-resolve 的话就找不到了,另外也有包默认自己就是这样的幽灵位置(比如 dumi ),从自己的包以 __dirname
为起点开始向上寻找东西和做信息判断,虽然这种方式不严谨,但是可以说共识的一种意识,能说他真的错了吗?
多依赖冲突问题
另外就是多依赖版本问题,因为隔离的严格,所以你打了多份同依赖的可能性是非常大的,比如多个 lodash
,这个问题一旦出现,只要不是 react
/ react-dom
/ antd
这种至关重要的基设,即使有多份也不会影响你的应用的,只是体积变大了,但是还是一个不爽的点,毕竟我们在 yarn 中有最终的绝招可以 resolutions
强制手动合并指明唯一版本,可见:
《 活用 yarn resolutions 统一版本大幅减小产物包体积(优化之最后的倔强)》
关于这点优化的倔强我也曾探索过自动化识别 resolutions 依赖的方案( yarn-resolutions ),但是也是结果好坏参半,这里不细谈,所以其实 yarn 是有逃生舱的。
但是 pnpm 在这块的最终做法就是手动提升:public-hoist-pattern
如果硬性说 pnpm 没给你开逃生窗口也不对,毕竟连完全隔离都不要了 shamefully-hoist
这种完全放弃隔离回到 yarn 幽灵时代的选项都有,可谓是生怕你用的不爽了。
社区普及性问题
其实随着 vue / vite 的仓库开始表态迁移到了 pnpm 管理,也更加认可了 pnpm 在 速度 上的优势,因为软链快复用好这点有目共睹,但是像 vue 这种全仓一个第三方依赖没有全都自己造的基础库,根本没用到 pnpm 为你提供的那些隔离的主打功能特性,可谓是用了个假的 pnpm ,另外其实 vue 仓库根本没全部使用 pnpm ,构建链路里还是混淆了 yarn ,也是个不纯的 pnpm 用法。
我们不提知名的这些有歧义的实践,就以普及性来说,其实还是很低的,你可能看到很多人自己在用,但是社区公共设施对 pnpm 安装依赖的支持还是很低,比如 cra 其实就不支持 pnpm 默认安装依赖,默认优先走 yarn ,也就是说作为一个后来居上的 “第三者” ,还有很长的路和很多要被质疑的困难和挑战要走。
下面我们聊聊 monorepo 上的 pnpm 表现。
pnpm monorepo
peerDependencies 困境
我们先聊最大的问题,就是 pnpm 在 peerDependencies
域是弱到极致了,贴紧业务来说,热更新是一个首要问题,那就是不要走大部分 lerna monorepo 的老路,每次改别的包的代码都要重新打包一次,所以我们的解法还是 alias
定位 + babel includes ,关于这点可见:
《 为什么使用 pnpm 可以光速建立好用的 monorepo(比yarn/lerna效率高)》
热更新我们可以解决,但是我们忽略了一个问题,就是在那个子包的代码使用第三方依赖的时候,总会优先使用它自己的 node_modules
,因为我们预期是这些第三方依赖在开发时 devDependencies
,而到了生产就 peerDependencies
,但实际上 pnpm 会给你安装 devDependencies
系依赖,因为你开发要用到对吧,那就铁定会去用到那个子包的第三方依赖的实例,而不会因为配置了 peerDependencies
就走本体应用引用处的第三方依赖。
因为查找依赖是向上走的,所以你就算把那个子包的 node_modules
删了,他也不会走本体项目,而走到最外层的全局 node_modules
,所以我们就忍痛要提升,不提升有什么后果?多实例小则增加包体积,大则直接可以让应用 crash,像 react
/ react-dom
这种是绝对多不得的,多实例 hooks 应用直接会崩溃,像 antd
这种多实例增大体积不说,message
的流会断掉,因为你本体的 message
和那个子包里用的 message
不是一个实例,也就会出现信息提示不按顺序来互相覆盖重叠提示的问题,这些都是致命的。
我们最终的妥协解法就是 alias 定位那些会致命的依赖到本体应用,再做一波 public 的全局提升做个保险,相关详细介绍可见:
《 pnpm monorepo 之多组件实例和 peerDependencies 困境回溯 》
虽然可以解决致命问题,但是我们还是承受了某些第三方依赖的多实例增加产物体积大小的副作用。
我遇到过一个社区开发者曾描述过:既然你使用 monorepo 的跨子包组件库就应该先发包再从 npm 上安装使用。
试想一下,假如这就是 pnpm 或者说 lerna 系的缺点,遵守这个公约共识确实可以解决问题,那么其实发包的繁琐性和成本性,还不能热更新即时生效就很难堪了。
我们再思考一下是不是自己的打开方式出错了?或许不应该把封装好的依赖第三方组件库的 HOC 放到一个独立子包里,这不符合 monorepo 的清真管理各个独立的子包的定义,造成耦合了,时至今日,我也一直在苦苦深度思考中,自己是不是做错了,换一种方式会不会更好?这么做的意义是什么?形成了沉淀了吗?
对比 yarn worksacpe / lerna 系
我们在之前的文章中也大幅赞扬了 pnpm 在创建 monorepo 上的简洁、效率性,而且不需要耦合 lerna 这么大不清真的工具,更重要的是可以严格隔离依赖,挤一堆复杂依赖的大型应用都不会有问题,nice 的不行!
这里再痛击一下,把 lerna 丢了,但是 lerna 的功能我们又需要怎么办?有人说那我要 MS Rush 起来了,也很火,其实 Jira 的公司 atlassian 有一个专门用来管理依赖版本和发布的工具叫 changesets ,对比下缺失的 lerna 功能,依赖提升管理这块 pnpm 帮你内置做好了,只剩下发包和版本控制流了,而 changeset 的好处是他不仅仅是一个版本控制、快捷发布的工具,更是一种 协作流 ,有自己独立的合作方法论,恐怖的是内置 CHANGELOG 生成还非常好用,github bot ,actions 都一应俱全,而在 lerna 里 CHANGELOG 都是很费劲的。
近期我也有幸参与过 changesets 底层工具和其本体的 pull request 中, 并且进行了一些大规模的管理发布实践(内网和公网都有),其易用性和便捷性简直是伟大,虽然还存在一些小问题,但核心功能真的 nice 到不行。俨然变成了 pnpm + changesets 是标配了。
有人会说了,我就是要质疑一下这个有小 bug 还 star 不高的项目,可我们发现 pnpm 的本体仓库都在用 changeset 进行管理版本和发布控制,好了,多说不宜,其意自见。
总结
正如本文开头所说的,pnpm 的功是大于过的,承受一点心智压力解法和一些项目体积去换取一个
速度、复杂应用 monorepo 依赖隔离
的绝赞优点是会上瘾的,用了一次就会用第二次,毕竟这种严格的依赖隔离可是仅仅 pnpm 有的 brand 功能,这里就不再吹捧 changesets 搭配了,changeset 是一个解耦的独立工具,用在 lerna 系等 monorepo 也可以。
看到这里,相信读者们已经对 pnpm 的技术选型临界点(Critical adoption)有一个自己的前备知识和初步思考了。
最后还是回归我最初提到的,pnpm 不是银弹,打不死的是信念,绕不开的是变化,可能最宝贵的还是追求 更高、更好 的探索精神吧。