rush管理monorepo理论及实践

3,878 阅读4分钟

What is monorepo

简单来说,就是一个git仓库管理某个范围的所有代码

Why monorepo

  • 源码透明度
  • 级联发布(如babel)
  • 代码复用
  • 配置复用(可hoist到上层,子项目继承)
  • 强制沟通(如公共库只能提供最新版本,库开发者和使用者联系更紧密)
  • ...

rush

rush官方文档

简单来说,rush提供了大型仓库(如monorepo)的管理能力

同时rush支持底层使用pnpm进行包管理,解决Phantom dependency、NPM doppelgangers等问题

rush基本使用

rush init

将项目初始化为rush管理

rush update(在rush.json的目录或其任意子目录执行)
  1. 更新common/config
  2. 检查所有项目的package.json,对比仓库的common shrinkwrap文件,确定其是否有效,如果过期了则更新shrinkwrap文件
  3. 所有依赖都安装到common/temp/node_modules
  4. 最后,rush为每个项目创建一个node_modules目录,并创建symlinks软链到common/temp/node_modules

rush update可能更新lockfile,rush install则会按lockfile安装

rush update --purge(强制重新安装依赖而不是基于缓存)

什么是shrinkwrap文件(如package-lock.json)

通过在git管理的一个大文件中保存一份完整的依赖安装计划解决依赖安装的不确定性问题

rush add

安装依赖

rush add -p webpack --dev    // -p指定要安装的包,这里将webpack安装为子项目dev依赖
rushx

相当于npm run

rush build(在rush.json的目录或其任意子目录执行)

rush build会执行所有项目npm script中的build

pnpm基本原理

依赖文件统一提升到上层,子项目中node_modules中使用软链依赖树(参考前文中rush update的依赖安装细节)

正确的依赖树结构(相较于扁平化依赖)确保了依赖使用的安全性(如项目中引入的库一定是package.json中所声明的依赖)

同时软链到上层统一管理的依赖,可以防止依赖的重复安装

Phantom dependency

简单来说,就是代码中可以加载到未在package.json中声明的依赖库

如A依赖B、C,同时B、C都依赖D

则npm会进行依赖扁平化来复用D,即安装后为B、C、D都在A的node_modules下最外层

现在A中可以直接依赖D,虽然D不在A的package.json声明的依赖中

影子依赖带来的具体问题
  • 不兼容的版本

假设我们的代码依赖了A并指定为^3,A中依赖了B

当我们的代码直接引入B时,B的具体版本我们将一无所知,因为B的版本完全由A的开发者控制

假设A进行了一个patch升级,则仍符合我们指定的版本范围,但如果A中对B的版本指定为主版本的升级版本,则可能导致我们代码中引入的B出现不兼容

  • 依赖丢失

假设我们作为库lib的开发者,lib中有一个dev依赖为A,A中依赖了B

当我们在代码中使用了B并发布出去,使用方install lib时不会安装lib的dev依赖,即不会安装A,则A的依赖B自然也不会安装

此时我们的库由于无法引入B而出现异常

同时,由于扁平化依赖的原因,可能使用方在安装其他库时也隐式安装了B,导致我们的库虽然缺失了B的依赖声明仍能正常工作

NPM doppelgangers

四个依赖A、B、C、D,其中A、B依赖E@1,C、D依赖E@2

则可能的情况为

  • 先扁平化安装了E@1,则C和D的node_modules中都会安装E@2
  • 先扁平化安装了E@2,则A和B的node_modules中都会安装E@1

影响

  • 重复安装依赖(影响安装速度和磁盘空间)
  • 影响打包体积(重复包都会打进来)
  • 可能破坏三方库内部的单例模式(E中可能有些逻辑依赖单例,而重复的E被依赖时可能分别引入不同的实例)
  • ts类型冲突
  • ...

子项目间依赖

依赖方dependencies里指定workspace:*(pnpm提供workspace协议)

如子项目app依赖子项目libs,则在app的package.json中

"dependencies": {

    "libs": "workspace:*"

}
构建产物依赖

前置build libs

rush build -T .    // -T .表示构建当前子项目所有依赖项目(排除自身)

libs的package.json中main指向产物入口

{

    "main": "./dist/main.js"

}

app引入libs时和普通三方库一致即可

源码依赖(两种方式)
  • libs的package.json中main字段指定包入口,app直接引入

libs的package.json

{

    "main": "./src/index.js"

}

app中引入

import * as libs from 'libs'
  • 直接用相对路径引入
import * as libs from '../../libs/src'