Monorepo 入门(keng)指南

背景

无论是开发应用还是二方库,monorepo 确实带来很多方便之处,之前一直使用的是 lerna + npm 作为 monorepo,由于公司无法使用 pnpm 也受限于 node版本的限制(部署阶段)

所以一直没有用上pnpm 和 turborepo/nx,但是最近想新开一个开源项目,想趁着这次机会做一次相对完整的调研,弥补当时的遗憾。

这篇文章是我了解到的 monorepo 相关的方案,如果你也需要对你的项目进行改造,我相信这篇文章应该能帮助对monorepo有一个初步的认识。

如果你在工作中已经使用monorepo,你也可以在评论区聊聊你使用monorepo有哪些痛点。

目标

我们先给自己制定一个目标

  • 了解有哪些方案
  • 每个方案有哪些功能
  • 它们适用于什么样的场景

Demo

我们先来直观的看一下monorepo 长什么样

以上是我们创建的一个monorepo

由两部分组成

  1. 有一个根项目,他本身也是一个package
  2. 在根项目下 有多个子package,通常放在packages/apps目录下,当然也可以用其他的命名

monorepo与普通的package最大的区别就是,在一个package下包含其他的package

monorepo 项目具备两个优点

  1. 本地的相互依赖

    在monorepo,如果你在packageA 中安装packageB,安装的事实上是本地的packageA,在packageA的node_module下packageB只是一个软链接

  2. 一键运行多个package的命令

    在根项目中运行一个命令,会自动运行子项目中所有相关的命令,这样可以达到一键启动多个项目,避免手动启动多个项目,或者一键安装所有项目的依赖。

想要具备上述两个功能,我们必须要启用npm包管理的特性workspace,npm包管理发展至今,pnpm、yarn、npm 都具备workspace特性,那我们来看看这workspace 是什么?如何开启?

WORKSPACE

Workspaces 是一个通用术语,它指的是 npm cli 中的一组功能,提供了管理多个包的支持,这些包来自于您本地文件系统中的一个顶级根包。我们还将这些在 npm install 期间自动建立符号链接的包称为单个工作空间,这意味着它是当前本地文件系统中显式定义在 package.json workspaces 配置中的嵌套包。

上述是npm对workspace的定义

  1. workspace npm用于管理多个包的特性。
  2. workspace 需要在package配置,目的是为了告诉npm,哪些package 组成这个workspace

从上述我们可以看出来,workspace 是monorepo的基础。

npm workspace

配置

jsx 复制代码
// package.json
{
  "name": "my-workspaces-powered-project",
  "workspaces": [
    "packages/module-a",
		"packages/module-b"
  ]
}

安装某个依赖

jsx 复制代码
npm install lodash -w react-app
npm install module-a -w react-app
npm install

运行某个package的命令

jsx 复制代码
// 在module 下运行test
npm run test --workspaces=module-a

// 运行所有pkg 的test
npm run test --workspaces

运行结果

这是运行test的结果,我们发现运行是线性的,虽然这几个package的test没有任何关系,很显然如果可以并行效率会更高。

看到这里你应该知道什么是 monorepo 了,而且怎么用 monorepo 应该有一定的认识了,除了npm之外其他包管理也有同样的功能,如果你好奇的话,不妨一起比较一下。

yarn workspace

配置与npm相同,但是命令有所不同

jsx 复制代码
yarn workspaces run test
npm run test --workspaces

pnpm workspace

  1. 配置文件不同

pnpm-workspace.yaml

yaml 复制代码
packages:
  # all packages in direct subdirs of packages/
  - 'packages/*'
  # all packages in subdirs of components/
  - 'components/**'
  # exclude packages that are inside test directories
  - '!**/test/**'
  1. 协议不同

    pnpm 支持协议 workspace: 这样更加直观,缺点就是你用npm 安装的话就会失败

yaml 复制代码
{
    "dependencies": {
        "foo": "workspace:*",
        "bar": "workspace:~",
        "qar": "workspace:^",
        "zoo": "workspace:^1.5.0"
    }
}

看到这里,你对workspace 应该有了比较直观的认识

我们可以总结一下,workspace 是包管理工具特性,方便monorepo 中依赖管理与命令的运行,

通过workspace 可以搭建一个简单的 monorepo,当然这只是入坑的第一步,如果真的只是启用workspace你会发现很多坑等着我们去踩。

版本管理

monorepo 修改一个package 与之相关的package的版本号也需要修改,我们想象一下:

有三个模块A、B、C

他们的关系是这样的

A依赖于B,B依赖于C ( A→B→C)

为了修复A中的bug,我们必须要对C做出改动,即便B不做改动,B中C的版本也需要升级,

如果我们手动维护A、B、C的版本号是一件非常累也很容易出错的事情。

因此我们需要引入新的工具来实现版本的自动管理 lerna/changeset,这两个工具有所不同

lerna 包含了workspace 的部分功能(安装依赖),同时也包含 版本的管理

changeset 不仅包含了版本的管理,同时也会生成修改日志,这对于一个好用的 npm package来说至关重要。pnpm 在官网就推荐使用 changeset

任务管理

随着monorepo 中package的增加,运行所有项目的build 可能时间会随之增加

假设我们本来只需要修改moduleA,moduleA 只依赖于 moduleB,与 C、D、E 毫不相干,但是我们为了在开发是调试我们有两种选择

  1. 构建moduleA和moduleB
  2. 构建所有的package

两者看上去都不是很好的方案,所以我们需要一种更聪明的运行命令的方式,那就是 nx 或 turborepo

他们是通过什么机制实现运行任务时间加速的呢

本地缓存

如果package运行过一次,并且这个package所依赖的package都没有发生过任何改动,那么基本可以断定这个package build 的结果也不会发生变化,直接采用上次 build 的结果这样就实现了执行任务的加速

共享缓存

nx 或 turborepo 都支持将构建的结果上传只云端,这样如果同事小强如果曾经构建过某个package,并且我没有任何改动,那么我就无需重新构建,nx/turborepo 会自动从云端下载缓存供我使用

任务编排

假设我们有一个如下图所示的monorepo

ssr-server-headless 不依赖于任何项目

  1. 所以 ssr-server-headless 的构建完全可以与其他任务同步进行
  2. news-app 的构建必须要求 norejs/ssr-cli 构建完成后才能构建

nx 和 turborepo 都可以通过编写任务之间的关系,确保任务有序、高效进行

除此之外必须介绍一下nx、turborepo 的更多特性,来感受一下他们能给我们带来什么

NX VS turborepo

NX

  • 依赖关系图可视化

    上图每个package之间的关系就是用nx生成的,在这一点上turborepo并不支持。

  • Affect

    NX支持探测当前改动,只编译你的改动相关的 package, 例如你只改动了packageA,那么packageA 与 依赖了packageA的所有package 会运行构建,turborepo也支持这一点

turborepo

  • 环境变量探测

    编译结果有时候会根据当前机器的环境变量编译出不同的结果,比如NODE_ENV 我们通常就放在环境变量中,如果我们在本地的环境变量NODE_ENV= local,编译的结果被缓存起来,部署过程中如果直接采用了 我们本地编译的结果,那最终的结果是灾难性的。turborepo 提供了环境变量的探测,如果两次编译的环境变量不同,即便源代码相同也不会采用缓存,当然具体探测哪些环境变量需要手动配置

对比图

NX turborepo
本地缓存 支持 支持
远程缓存 支持 支持
自定义远程缓存服务器 不支持(开发中) 支持(有开源项目)
环境变量探测 不支持 支持
依赖可视化 支持 不支持
Affect 支持 支持

到这里我们的monorepo 在 nx 和 turborepo 的加持之下变得更好用了,我们实现了运行的加速,我们实现了运行任务关系的绑定,避免执行多余的命令。

局限性

一切看上去距离完美更近了一步,但是仔细想一下它还是有一些缺点

脆弱的缓存

假设我们只是修改一个环境变量或者一个 package ,与之相关联的 package 缓存就会失效,缓存非常容易被丢弃。

可能误用缓存

由于是否采用缓存 nx 和 turborepo 都是采用源代码计算 hash 作为是否采用缓存的依据,

假设我们有个package,它依赖于根项目中的一个文件.env,如果.env 中的变量修改了,nx 会认为项目源代码并没有发生改变,从而缓存被复用,所以在使用是需要考虑到这一点,避免这种事情发生。

DX的提升有限

在开发阶段通常情况下需要 watch,并且有时候没有导出文件,所以 nx/turborepo 无法缓存结果,这种情况下对于开发体验的提升变得比较有限

小结

综上所述,nx 和 turborepo 对于加速 monorepo 的构建在使用得当的情况下,有事半功倍的效果,虽然某些特殊情况下容易出错,并且有些情况下缓存帮助不大,但是在任务编排方面的作用是毋庸置疑的。

nx和turborepo中的缓存粒度比较大,都是以package为维度进行缓存,如果想要更快的开发构建速度,可能需要更小粒度的缓存,turbopackage 以函数作为缓存粒度,这将会大大减少重复的构建,但是目前它还在开发中。

总结

  1. workspace 是 monorepo 的基础,便于安装本地的依赖,运行命令,npm,yarn,pnpm 都支持
  2. 为了便于发布和版本管理我们需要引入lerna/changesets ,但是更推荐使用 changesets
  3. 如果package比较多,可以利用nx/turborepo 来加速构建,避免构建不必要的package,但是使用时需要注意避免误用缓存,即便不用缓存特性,对于任务编排而言,这两者都是很好的工具。
相关推荐
qq_3901617718 分钟前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js