Monorepo最佳实践之Yarn Workspaces

·  阅读 8985
Monorepo最佳实践之Yarn Workspaces

Yarn Workspaces(工作空间/工作区,本文使用工作空间这一名称)是Yarn提供的Monorepo依赖管理机制,从Yarn 1.0开始默认支持,用于在代码仓库的根目录下管理多个project的依赖。
Yarn Workspaces的目标是使使用Monorepo(Monorepository)变得简单,以一种更具声明性的方式处理yarn link的主要使用场景。简而言之,它们允许多个项目共存在同一个代码库中,并相互交叉引用,并且保证一个项目源代码的任何修改都会立即应用到其他项目中。

Monorepo

无论是维护npm包还是开发大型前端系统,都存在在不同功能模块中管理着多个功能相近的包,或者这些包之间存在依赖关系的场景。如果将这些包拆分在不同仓库里,那么面临要跨多个包进行更改时,工作会非常繁琐和复杂。
重复安装、管理繁琐的缺点从npm package诞生起便一直存在,node_modules hell就是该问题的集中体现。

npm size.png

为了简化流程,很多大型项目采用了Monorepo的做法,即把所有的包放在一个仓库中管理

Babel、React、Vue、Jest等都使用了monorepo的管理方式。

Menorepo的优点是可以在一个仓库里维护多个package,可统一构建,跨package调试、依赖管理、版本发布都十分方便,搭配工具还能统一生成CHANGELOG;
代价是即使只开发其中一个package也需要安装整个项目的依赖。以jest为例,其Monorepo代码结构为:

| jest/
| ---- package.json
| ---- packages/
| -------- babel-jest/
| ------------ package.json
| -------- babel-plugin-jest-hoist/
| ------------ package.json
| -------- babel-preset-jest/
| ------------ package.json
| -------- .../
复制代码

Yarn Workspaces

一些概念

  • workspace context:in the context of the workspace feature, a project is the whole directory tree making up your workspaces (often the repository itself) 工作空间上下文:工作空间所在的文件目录的一个个子目录就是工作空间的上下文,整个目录树构成了工作空间实体。
  • workspace:A workspace is a local package made up from your own sources from that same project 工作空间是一个本地代码包,由同一项目的源代码组成。
  • worktree:a worktree is the name given to workspaces that list their own child workspaces. 工作树是列出它们自己的子工作空间的工作空间的名称。
  • workspace-root:A project contains one or more worktrees, which may themselves contain any number of workspaces. Any project contains at least one workspace: the root one. 一个项目包含一个或多个工作树,这些工作树本身可以包含任意数量的工作空间。任何项目都至少包含一个工作空间:workspace-root(根工作空间)。

为何使用Yarn Workspaces

在以Monorepo为代码组织方式的项目中,依赖管理的规模和复杂度均有不小的提升(这也不难理解,随着”数量“的增加,任何小的问题都会变得复杂)。
如何减少依赖重复安装?如何优雅实现跨目录代码共享?如何对依赖版本进行统一管理以避免版本冲突?
所以这些问题都可以借助Yarn Workspaces来解决!
Yarn官方对于Yarn Workspaces的使用时机(Why would you want to do this?)是这样描述的:

  • Your dependencies can be linked together, which means that your workspaces can depend on one another while always using the most up-to-date code available. This is also a better mechanism than yarn link since it only affects your workspace tree rather than your whole system. 工作区内的依赖关系可以链接在一起,这意味着工作区可以相互依赖,同时始终使用最新的可用代码。这也是一个相对于yarn link更好的机制,因为它只影响你的工作空间树,而不是整个系统。
  • All your project dependencies will be installed together, giving Yarn more latitude to better optimize them. 所有的项目依赖关系都将被安装在一起,为Yarn提供更多的自由度来更好地优化它们。
  • Yarn will use a single lockfile rather than a different one for each project, which means fewer conflicts and easier reviews. 对于每个项目,Yarn将使用一个公共的的锁文件而不是为每个工程使用一个不同的锁文件,这意味着更少的冲突和更容易的版本审查。

在Workspace作用空间下的任何一个目录下执行yarn命令都会安装整个Workspace的依赖。

如何启用Workspace

首先,需要确保项目中安装有yarn,怎么安装呢?当然是借助npm了(当未来某一天node.js同时内置了npmyarn时就不需要这么麻烦了)。

npm install yarn --save
or 
npm install yarn --global // 全局安装
复制代码

然后在项目根目录的package.json中增加如下配置:

{
  "private": true,
  "workspaces": ["app1", "app2"] // 具名Workspace,名字任意,但需跟子项目中package.json中name属性值一致
}
// 当Workspace很多时,也可以采用全目录引用的方式
// 假设项目代码在projects目录下
{
  "private": true,
  "workspaces": ["projects/*"]
}
复制代码

Note that the private: true is required! Workspaces are not meant to be published, so we’ve added this safety measure to make sure that nothing can accidentally expose them.
注意:private: true配置是必需的!工作区并不意味着要被发布,所以需要添加了这个安全措施,以确保不会发布到npm仓库。

之后,创建两个名为app1app2的子文件夹。在其中的每个文件中,分别配置如下package.json文件:
app1/package.json:

{
    "name": "app1",
    "version": "1.0.0",
    "dependencies": {
        // ***
    }
}
复制代码

app2/package.json:

{
    "name": "app2",
    "version": "1.0.0",
    "dependencies": {
       // ***
    }
}
复制代码

最后,就可以开始在workspace下开发app1、app2项目了。安装项目依赖时,为了方便后续开发可以在项目根目录执行yarn install,这样就会安装整个Workspace的依赖,后续无论是在app1还是app2都不需要重新安装。 执行完yarn i之后上述示例workspace的目录结构为:

/package.json
/yarn.lock
/node_modules

/projects/app1/package.json
/projects/app1/yarn.lock
/projects/app1/node_modules

/projects/app2/package.json
/projects/app2/yarn.lock
/projects/app2/node_modules
复制代码

使用yarn workspaces info [--json]命令可以获得整个workspace的目录结构:

// yarn workspaces v1.22.10
{
  "app1": {
    "location": "projects/app1",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "app2": {
    "location": "projects/app2",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  }
}
复制代码

说明:

  • projects 是各个子项目的上级目录,即上文中的 workspace-root,而 app1 和 app2 称之为 workspace
  • yarn install 命令既可以在 workspace-root 目录下执行,也可以在任何一个 workspace 目录下执行,效果是一样的;
  • app1 和 app2 目录下的 node_modules 目录不是必然存在的,只有在app1 和 app2 依赖了不同版本的同一个依赖或者存在自己的特有依赖时才会出现;
  • workspaceDependencies列出了该workspace依赖的其他workspace,因为app1,app2没有依赖其他workspace,所以值为空数组。

Yarn Workspaces常用CLI

Yarn 1.x已有

yarn workspace

命令格式:

yarn workspace <workspace_name> <command>
复制代码

作用:在指定的workspace下执行command,即command作用域为某个workspace。示例:

# 为app1安装react
yarn workspace app1 add react --save

# 执行app1中的start脚本
yarn workspace app1 run start
复制代码

yarn workspaces info [--json]

作用:此命令将显示当前项目的工作空间依赖关系树。示例:

yarn workspaces info
复制代码

yarn workspaces run <command>

作用:将在每个工作区中运行所选择的Yarn命令,即遍历所有workspace执行command命令,示例:

# 在所有workspace中执行yarn start命令
yarn workspaces run start

# 在所有workspace中执行yarn test命令
yarn workspaces run test
复制代码

Yarn 2.x新增

注意:在Yarn 2.x中删除了yarn workspaces info指令

2021年9月6号,yarn发布最新版本:2.4.3。在该版本中增加了几个跟workspace相关的CLI。

yarn workspaces focus

要使用此命令,请先安装workspace-tools插件:yarn plugin import workspace-tools

作用:安装单个工作区及其依赖项。

yarn workspaces foreach [command]

要使用此命令,请先安装workspace-tools插件:yarn plugin import workspace-tools

作用:在所有工作空间上运行command命令(有点类似Yarn 1.x中的yarn workspaces)。示例:

yarn workspaces foreach run start
复制代码

yarn workspaces list

注意:需要在workspace-root下执行。 作用:列出所有可用的工作区。示例:

yarn workspaces list
复制代码

image.png

Yarn Workspaces实践

Yarn Workspace的典型实践便是在workspace之间的互相link引用,其省去了繁琐的 npm 发布步骤,本地引用基于yarn link软链到本地source code,代码的修改会实时在引用它的workspace中生效。接下来将通过一个实例来展示Yarn Workspace的强大之处,示例代码已上传至github。

demo项目的主要实践有:

  • 通过 yarn workspace link 仓库中的 components;
  • 使用 yarn 作为 package manager 管理项目中的依赖。

代码目录:

├── components
│   └── hello-world # 公共组件
│       ├── index.js
│       └── package.json
├── package.json
├── projects
│   ├── app1 # react脚手架项目
│   │   ├── config
│   │   ├── package.json
│   │   ├── public
│   │   ├── scripts
│   │   ├── src
│   │   └── yarn.lock
│   └── app2 # react脚手架项目
│       ├── package.json
│       ├── public
│       ├── src
│       └── yarn.lock
└── yarn.lock
复制代码

在workspace root下有三个workspace:app1、app2、hello-world,其中app1和app2是create-react-app生成的react脚手架项目,hello-world是一个简单的react组件。三个workspace的package.json配置为:

// app1
{
  "name": "app1",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
     "react": "^17.0.2" 
     //...
   }
}
// app2
{
  "name": "app2",
  "version": "1.0.0",
  "private": true,
  "dependencies": { 
    "react": "^17.0.2" 
    // ...
   }
}
// hello-world
{
  "name": "hello-world",
  "version": "1.0.0",
  "description": "hello-world",
  "main": "index.js",
  "dependencies": {
    "react": "^17.0.2"
  }
}
复制代码

根目录的package.json配置启用了yarn workspace:

{
  "private": true,
  "workspaces": [
    "projects/*", // 扫描projects目录,将项目纳入到workspace context中
    "components/*" // 扫描components目录,将项目纳入到workspace context中
  ],
  "name": "version-demo",
  "version": "1.0.0"
}
复制代码

通过yarn workspaces info --json查看当前worktree:

{
  "app1": {
    "location": "projects/app1",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "app2": {
    "location": "projects/app2",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "hello-world": {
    "location": "components/hello-world",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  }
}
复制代码

使用components中的组件

在Yarn Workspaces作用域下,workspace的互相引用变更简单起来,现在要在app1中使用components/hello-wrold组件,只需两步即可完成。

  • 第一步:使用yarn | yarn workspaces命令“安装”依赖
cd projects/app1 &
yarn add hello-world@1.0.0 # 指定version,确保准确命中
or
yarn workspace app1 add hello-world@1.0.0 # 在root中指定workspace,进行安装
复制代码

安装完毕后再使用yarn workspaces info --json查看当前worktree:

{
  "app1": {
    "location": "projects/app1",
    "workspaceDependencies": [
       "hello-world"
    ],
    "mismatchedWorkspaceDependencies": []
  },
  "app2": {
    "location": "projects/app2",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "hello-world": {
    "location": "components/hello-world",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  }
}
复制代码

可以发现app1workspaceDependencies依赖增加了一项:hello-world

// paths.js
module.exports = {
  // ...
  componentsPath: resolveApp('../../components'),
};

// webpack.config.js
{
   test: /\.(js|mjs|jsx|ts|tsx)$/,
   include: [paths.appSrc, paths.componentsPath],
   loader: require.resolve("babel-loader"),
   ...
}
复制代码
  • 在app1中执行yarn start效果如图:

image.png

components软链过程

  1. 判断当前 Monorepo 中,是否存在匹配 app1 所需版本的 hello-world
  2. 若存在,执行 link 操作,app1 直接使用本地 hello-world
  3. 若不存在,从远端 npm 仓库拉取符合版本的 hello-worldapp1 使用;
  4. 若远端 npm 仓库也找不到 hello-world 组件,则报错:error An unexpected error occurred: "https://registry.npmjs.org/hello-world: Not found".

Yarn Workspaces限制与不足

  1. yarn workspace并没有像lerna那样封装大量的高层API,整个workspace整体上还是依赖于整个yarn命令体系;
  2. workspace不支持嵌套(只能有一个workspace-root);
  3. workspace采用的是向上遍历,所以workspace并不能识别workspace-root之外的依赖;
  4. 依赖引用版本不确定性,如上文的components/hello-world,当开发者将其发布到了npm仓库并且npm仓库的版本号跟本地版本号不一致时,容易出现此问题;
  5. yarn workspace link的依赖需要添加到webpack/rollup/vite等构建工具的构建路径中,增加构建时成本,发布变慢;
  6. yarn.lock 容易出现冲突。

Yarn Workspaces使用规范推荐

针对Yarn Workspaces存在的上述问题,推荐一套项目级别的实施规范。以利用其长处,避免其短处。

  1. yarn workspace link的Packages设置为私有

在需要本地依赖的组件的package.json中设置private:true,目的是防止其被误上传至 npm 远程仓库。强制限定只能通过本地源码link的方式引用,这样就可以有效避免问题4。

  1. 所有workspace的依赖声明收敛到workspace-root中

即各个workspace的pakcage.json中不再声明dependencies、devDependencies,依赖的注册全部收敛到workspace-root的package.json中。
对于新的项目可以在一开始就只使用yarn -W add [package] [--dev]进行安装,对于历史项目可以手工修改各workspace的package.json,将依赖剪切到根目录的package.json里。
这样做的好处是统一各个子workspace的依赖版本,避免同一依赖安装不同版本,保持整个worktree下项目里依赖版本的统一,并在各workspace间共享依赖。
对于全局共享依赖带来的项目初始化安装时间的增加,是可以接受的,因为只是首次安装耗时长而已,后续因为有缓存,安装时间将大大缩减。

  1. 禁止workspace独立新增依赖

所有新增的依赖需要通过yarn -W add [package] [--dev]进行安装,这一条可以认识是对第二条规范的补充。

  1. yarn.lock必须提交,冲突必须解决

yarn.lock是yarn依赖版本控制的基础,在全局共享依赖的框架下,yarn.lock文件的维护变得尤其重要。保证提交是为了确保新增依赖能够纳入到yarn版本管控中,正确解决冲突则是确保依赖版本的唯一性、统一性。

  1. 在Yarn Workspace跟目录中添加eslint、prettier配置,各子workspace继承root的配置

eslint和prettier配置已经成为规范项目代码的标配,既然是配置当然要在整个Workspace范围内保持统一。

  1. 日常开发应当在workspace-root目录下

这是对规范5补充,确保全局代码规范能够生效;
也能让开发者方便查看yarn workspace link的Packages

参考

Yarn 1 Workspace
Yarn 2 Workspace
monorepo-lerna-yarn-workspaces
应用级 Monorepo 优化方案

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改