前端项目做大了,试试monorepo

5,770 阅读4分钟

第一次接触monorepo是去年年初,说来非常巧合,当时有个活动页的需求,因为webpack配的比较菜,听一哥们说parcel开箱即用写活动页无敌,然后我就去看了。看到parcel用了lerna,这激起了我的好奇,觉得这东西挺玄学,一条命令能帮你安装指定工程下的所有依赖,正好当时在学nodejs也写了一些工具,比如国际化翻译工具、本地mock server、前端页面监控等,把这些工程放在一起,用lerna包起来,只需install一次,就可下载所有依赖,这样就实现n个项目变成一个项目,形成闭环。

当然上面的理解是完全不够的,monorepo顾名思义mono + repository多个仓库并成单一仓库。monorepo的使用场景是多模块相互依赖,或者n模块有共同依赖的模块,那么相互引用必然非常麻烦,毕竟谁也不愿意看到一大堆相对路径,现在随着ts的普及,业务开发重度依赖代码提示,如果本身n个模块各自就是独立的项目or库,那tsconfig中的path路径该如何配呢?更要命的是,如果A模块引入了第三方库,而core模块没有,core恰好引入和A的那个方法,还得给core手动yarn add depxxx,如果出现类似这种很凌乱的情况,那就不要犹豫,直接上monorepo吧,接入方便,只是在外包了层壳。

yarn workspace

市面上monorepo的解决方案不少,最主流的是yarn workspace + lerna,毕竟两大新坑vue3element+也是这么做的,依旧是以解决问题的角度出发,来看下workspace能给我们带来什么?我对yarn官网的例子进行了拓展:

root
|---package.json
|———packages/
    |
    |————workspace-a/
        |---node_modules/
        |---package.json
    |
    |————workspace-b/
        |---node_modules/
        |---package.json
// workspace-a/package.json:
{
  "name": "workspace-a",
  "version": "1.0.0",

  "dependencies": {
    "cross-env": "5.0.5"
  }
}

// workspace-b/package.json:
{
  "name": "workspace-b",
  "version": "1.0.0",

  "dependencies": {
    "cross-env": "5.0.5",
    "dotEnv": "1.0.0",
    "workspace-a": "1.0.0"
  }
}

上面两个独立的工程,我们在workspace-brequire dotEnv,在workspace-arequire b,现在会报错,因为workspace-a中没有dotEnv 解决方案,在rootpackage.json中添加:

{
  "private": true,
  "workspaces": ["workspace-a", "workspace-b"]
}

private是防止publish root文件夹,workspace是指定工作区
yarn会把["workspace-a", "workspace-b"]中的dep依赖自动安装到root,并且和工程下的node_modules做映射,实现了一个syslink,其实也可以理解成link到了根目录,同时在根目录下的node_modules中也会出现两个包,workspace-aworkspace-b,因为link到了根,所以改变workspace-a中的文件,node_modules下的文件也会跟着变,试想,我某一个模块是放common ui组件的,剩下a b c d ...模块是业务模块,我只需要类似import ui from 'common-ui',特别是ts的加持,不需要关心引用路径的,统一当项目下的node_modules模块即可。
如果不用workspace,那得借助一大堆npm link,包多了就失去了可管理性。

lerna

bb了那么多的yarn workspace,似乎我们的痛点已经解决,那lerna又有什么作用呢?
这是lernacommand集合,大多比较鸡肋:

lerna bootstrap	安装依赖
lerna clean	删除各个包下的node_modules
lerna init	创建新的lerna库
lerna list	显示package列表
lerna changed	显示自上次relase tag以来有修改的包,选项通 list
lerna diff	显示自上次relase tag以来有修改的包的差异,执行 git diff
lerna exec	在每个包目录下执行任意命令

lerna run	执行每个包package.json中的脚本命令
lerna add       添加一个包的版本为各个包的依赖
lerna import	引入package
lerna link	链接互相引用的库
lerna create	新建package
lerna publish	发布

lernayarn workspace有一部分功能确实重复,毕竟先有lerna后有yarn workspace,既然可以解决项目管理的混乱,每一个包可以使单独的模块库,我们可以借助lerna做项目的构建和发布,同样配置也很简洁:

cd root
yarn add lerna
touch lerna.json
// lerna.json
{
  "packages": ["packages/*"],
  "npmClient": "yarn",
  "useWorkspaces": true,
  "command": {
    "create": {
      "homepage": "https://github.com/lerna/lerna",
      "license": "MIT"
    },
    "version": {
      "allowBranch": "main",
      "conventionalCommits": true,
      "exact": true,
      "message": "chore(release): %s"
    }
  },
  "version": "0.0.0"
}
// package.json
{
  "scripts": {
    "bootstrap": "lerna bootstrap",
    "build": "lerna run build",
    "publish": "lerna publish --yes from-package"
  }
  ...
}

我们首选需要在root下安装lerna,如上配置完npm script后接着运行yarn bootstrap,就会自动安装workspace中的依赖,和直接yarn一样,但是由于缓存,安装速率要比yarn快不少,以上是同workspace公共能力,我们更多会使用lerna的发布能力,运行yarn publish,做了如下事情:

  • 运行lerna updated来决定哪一个包需要被publish
  • 如果有必要,将会更新lerna.json中的version
  • 将所有更新过的的包中的package.json的version字段更新
  • 将所有更新过的包中的依赖更新
  • 为新版本创建一个git commit或tag
  • 将包publish到npm上(不一定是npm也可是私仓),package.json中加入publishConfig.registry选项配置地址

以上是比较粗略的monorepo实践,非常适合node工具集成方案、ui库等。现在几乎所有大型开源项目使用包管理模式,所以在平时写项目的同时是不是也考虑上一波呢~