pnpm monorepo之多组件实例和peerDependencies困境回溯

回溯

开门见山,pnpm 从渐入视野到使用已经进入成熟期。本文不会探讨 pnpm 的使用和 monorepo 方案,如需从零开始了解,可参见:

随着 lerna 的维护速度逐渐放缓(几个月才 commit 一次),vite / vue3 仓库迁移至 pnpm ,使用 pnpm 管理 monorepo 已是目前的最佳解之一

但是其中不免存在一些问题。

问题

pnpm 解的最优性

我们来探讨一个问题,正常情况下,使用 pnpm 去管理 workspace 是多见于基础组件库或者工具库,他们之间很少去使用第三方依赖或者自身就是依赖生产制造者,使用 pnpm 只是提供一个工作空间的 更快更便捷 的解而已。

那放眼到我们业务中使用,我们会选择 workspace + monorepo 方案往往是希望同时管理很多项目,而很多项目间的依赖又极其错综复杂,比如一个项目依赖 webpack4 ,另一个依赖 webpack5 ,这在 lerna 这种隔离不严格的 hoist 工具内,经常会产生依赖版本冲突,造成多层依赖上游链路混用 “隐形依赖” 的版本问题,最终导致项目跑不起来或者运行失败,最典型的一个例子就是 cra 的项目无法在 lerna 内 packages 内创建。

所以 pnpm 给我们提供了这个解法,就是 严格的依赖隔离管理 ,由于每个依赖严格的限制了自己只能使用符合自己版本的依赖,避免了 隐形依赖 的问题,从而实现了极其复杂的依赖版本交错场景下的适配性。

隔离的副作用

有没有想过一种场景,@scope/components 子包作为组件库,里面使用了 antd@^4.16.0 作为基础,而 @scope/app 作为主应用,使用了 antd@^4.16.13 作为基础,那么他又要去复用 @scope/components 的组件,此时 antd 版本获取该怎么办?

在早期 lerna 系的完全 hoist 思维里,我们认为 antd 实际上只需要取一个即可,那就是只取我们主应用 @scope/appantd 实例,因为我们会在 @scope/components 里将 antd 指定为开发依赖,不进入生产:

// packages/components/package.json
{
  "name": "@scope/components",
  "version": "1.0.0",
  "main": "dist/index.js",
  "devDependencies": {
    "antd": ">=4.16.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "peerDependencies": {
    "antd": ">=4.16.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

以对外发包的思维继续思考,本地开发使用 antd 没毛病,那发布到 npm 去给其他人使用时,由于只会安装生产依赖,所以不会安装 antd ,再配合 peerDependencies 提示,即可完美达成我们的目的。

但在 pnpm 的 workspace 内,devDependencies 是实实在在安装到 packages/components/node_modules 的,不然你如何开发和编译?( 比如 tsx )

因为只要存在 node_modules ,就会识别到进入严格的隔离,导致你的主应用使用的是 packages/app/node_modules/antd ,而组件库使用的是 packages/components/node_modules/antd ,从而造成多实例,引发 message 不成队列、封装的 ConfigProvider 等 Context 上下文不在一个实例,打包体积倍增等致命问题。

这还了得!其实这就是 pnpm 的 peerDependencies 困境,那如何解决?

解决

在这个问题上我们的解只有一个方向,就是保证 唯一实例

alias 定位法

给主应用 @scope/app 的 webpack 配置加上 alias 强制定位 antdreactreact-dom

  resolve: {
    alias: {
      'antd': path.resolve(__dirname, 'node_modules/antd'),
      'react': path.resolve(__dirname, 'node_modules/react'),
      'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
      // ...如果你有用到 react-router-dom 还需定位 react-router-dom 和 react-router
    },
  },

如此一来即可保证单实例,顺利解决上面的问题。

全局提升法

使用这种方式需要对 pnpm hoist 有一定理解:

  1. 当不同子包都使用同样版本的某个依赖时,该依赖会被默认提升至顶层 node_modules ,子包内的该依赖会软链到顶层,全局唯一实例。

  2. 当子包间存在某依赖不同版本的使用时,会进行隔离处理放至 .pnpm (一个 pnpm 存放依赖的 store),子包分别软链对应的版本实现隔离。

乍一看第一种情况几乎不可能,因为我们的项目繁多依赖版本号不可能可控。但是我们就是要手动提升他!

配置 pnpm 强制提升:

# .npmrc

# 强制提升所有 antd 到全局,保证唯一实例
public-hoist-pattern[]=antd

# 不配置这个选项时候的默认值,我们要手动把他加上
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=@prettier/plugin-*
public-hoist-pattern[]=*prettier-plugin-*

关于这个选项的说明可见:public-hoist-pattern

当手动配置时,其默认值需要我们主动给他加回去,其实这里默认提升了 eslint 系依赖到全局还是免去我们很多重复安装的功夫的,另外我们还可以提升 prettierlodash 等万年依赖。(还是不建议提升 react ,因为有可能版本不一致)

所以提升了 antd ,那么我们就可以使用魔法了:

// packages/components/package.json
{
  "name": "@scope/components",
  "version": "1.0.0",
  "main": "dist/index.js",
  "devDependencies": {
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "typescript": "^4.1.2"
  },
  "peerDependencies": {
    "antd": ">=4.16.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

此时由于存在 @types/* 包,react 可以顺利通过 typescript 编译,对外发布包时由于存在 peerDependencies ,也不影响完整性,在使用时,大家都会去找最上层的 node_modules/antd ,实现了全局单实例。

总结

综合以上解法来看,我们甚至可以同时使用这两种搭配,但实际上,我们无论怎么解决,当 @scope/components 的第三方依赖变得更多时,需要涉及一些生产包 dependencies 时,终究还是要安装到该组件库包内,而此时主应用又安装了相同的不同版本依赖,又会造成多实例重复打包问题(但没有 react 和 antd 这种致命),所以又要不断添加这些的 alias 或进行提升。

当然,我写个自动识别 @scope/apppackage.json 的脚本,看看他依赖了哪些 @scope 开头的依赖,然后去找这些依赖的 package.json 里的 dependencies 再 alias 批量定位回来可以不?可以是可以,但很麻烦。

事物的两面性就是这样,pnpm 官方关于 peerDependencies 的 issue 探讨也是久而未决,因为社区有影响力使用 pnpm 的库都是上文中所描述的那两种基础库,即没什么第三方依赖的纯净库,由于不是业务库,也无需面临严重的版本冲突,自然这个问题对 pnpm 来说就不再那么重要了。

退一步来说,因为这个问题我们回退到 lerna + yarn worksapce 的时代?

答案是绝对的 No ,已经没有人可以忍受版本不透明,隔离出现太多版本冲突,cra 都无法创建项目的时代了,所以请拥抱变化寻找更优解吧!

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
首先,让我们解释一下这些术语的含义: - Monorepo:将多个相关项目放在同一个代码库中的做法。 - PNPM:一种 Node.js 包管理器,类似于 npm 和 yarn。 - React:一个流行的 JavaScript 库,用于构建用户界面。 现在,我们来看一下如何在 Monorepo 中使用 PNPM 和 React: 1. 创建一个 Monorepo 项目,将所有相关项目放在同一个代码库中。 2. 在根目录下创建一个 `package.json` 文件,并添加以下内容: ``` { "private": true, "workspaces": [ "packages/*" ] } ``` 这个配置告诉 PNPM,这是一个 Monorepo 项目,并且它应该将所有 `packages/*` 目录下的包视为一个工作空间。 3. 在 `packages/` 目录下创建一个新的 React 应用程序。你可以使用 `create-react-app` 工具来快速创建一个新项目: ``` npx create-react-app my-app ``` 4. 确保在 `packages/my-app` 目录下有一个 `package.json` 文件,并添加以下内容: ``` { "name": "my-app", "version": "0.1.0", "dependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "react-scripts": "^4.0.3" } } ``` 这个配置告诉 PNPM,这个包依赖于 React 和 React DOM 库,并且需要从 `react-scripts` 包中获取开发依赖项。 5. 在 `packages/my-app` 目录下运行以下命令,安装依赖项: ``` pnpm install ``` 6. 在 `packages/my-app` 目录下运行以下命令,启动 React 应用程序: ``` npm start ``` 这就是在 Monorepo 中使用 PNPM 和 React 的基本步骤。你可以使用相同的方法添加其他包和应用程序,并使用 PNPM 管理它们的依赖项。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咲奈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值