使用 yarn workspace 管理多个项目

1,080 阅读5分钟

这个就是大家在网上看到的 monorepo ,其中 yarn workspace 是实现。

1. 当前存在的问题

  1. 相同功能组件重复实现的问题。在前端开发中一个组件在多个项目中重复出现,如果是不同的人负责不同的项目,更容易出现同样相同的组件却需要不同的人实现,耗费多倍的人力,如果通过拷贝的方式实现代码共享,那么发现组件有问题的时候就得需要反复复制。

  1. 公共逻辑无法统一管理的困境。一些公司会分 web 端和 app 端,但是呢两端的实现除了界面不相同以外,其余基本上都是相同的,也就是很多逻辑两端都实现了一遍。前后端开发中,特别后端是 nodejs ,且使用 ts 作为开发语言的时候, API 的类型在前后端都会定义一遍;同样的在表单的验证中,一般前后端都要做表单数据的验证,但当下由于在不同的项目,导致两端都编写了这样的逻辑。

2. workspace项目和普通项目架构对比

对于普通的项目,即便是相同的逻辑或组件都会各自的项目中保存一份。

flowchart TD
  subgraph "普通多项目结构"
      P1[项目A] --> N1[node_modules]
      P1 --> S1[src]
      S1 --> C1["组件
      • Button
      • Table
      • Form"]
      S1 --> U1["工具函数
      • format.ts
      • validate.ts
      • request.ts"]
      
      P2[项目B] --> N2[node_modules]
      P2 --> S2[src]
      S2 --> C2["组件
      • Button
      • Table
      • Form"]
      S2 --> U2["工具函数
      • format.ts
      • validate.ts
      • request.ts"]
      
      P3[项目C] --> N3[node_modules]
      P3 --> S3[src]
      S3 --> C3["组件
      • Button
      • Table
      • Form"]
      S3 --> U3["工具函数
      • format.ts
      • validate.ts
      • request.ts"]

      %% 标注重复部分
      style C1 fill:#ffebee,stroke:#c62828
      style C2 fill:#ffebee,stroke:#c62828
      style C3 fill:#ffebee,stroke:#c62828
      style U1 fill:#fff3e0,stroke:#ef6c00
      style U2 fill:#fff3e0,stroke:#ef6c00
      style U3 fill:#fff3e0,stroke:#ef6c00
  end

  %% 样式
  classDef project fill:#e1f5fe,stroke:#01579b
  classDef deps fill:#f5f5f5,stroke:#9e9e9e
  class P1,P2,P3 project
  class N1,N2,N3 deps

不管是编写这些逻辑或组件还是维护都需要花费多倍的时间。

对于 workspace 的项目来说,相同的逻辑或组件都会放在相同的地方。

flowchart TD
  subgraph "Workspace项目架构"
      ROOT[根目录] --> PF["package.json
      workspaces: ['packages/*']"]
      ROOT --> NM["共享 node_modules"]
      ROOT --> PKG[packages]
      
      %% 共享包
      PKG --> SHARED[共享模块]
      SHARED --> COMP["@shared/components
      • Button
      • Table
      • Form"]
      SHARED --> UTILS["@shared/utils
      • format.ts
      • validate.ts
      • request.ts"]
      
      %% 业务项目
      PKG --> P1[项目A]
      PKG --> P2[项目B]
      PKG --> P3[项目C]
      
      P1 --> S1["src
      • 业务组件
      • 业务逻辑"]
      P2 --> S2["src
      • 业务组件
      • 业务逻辑"]
      P3 --> S3["src
      • 业务组件
      • 业务逻辑"]
      
      %% 依赖关系
      COMP -..-> S1 & S2 & S3
      UTILS -..-> S1 & S2 & S3

      %% 高亮共享模块
      style COMP fill:#e8f5e9,stroke:#2e7d32
      style UTILS fill:#e8f5e9,stroke:#2e7d32
  end

  %% 样式
  classDef root fill:#e1f5fe,stroke:#01579b
  classDef project fill:#bbdefb,stroke:#1976d2
  classDef deps fill:#f5f5f5,stroke:#9e9e9e
  class ROOT root
  class P1,P2,P3 project
  class NM,PF,PKG deps

3. 流行库的项目结构

注:以下图都是 AI 帮助下生成,并不是基于当下最新的代码生成,仅供参考

其中前端使用很多的 vue ,其项目结构:

flowchart TD
  ROOT[vue-next] --> PF["package.json
  pnpm-workspace.yaml"]
  ROOT --> PKGS[packages]
  
  PKGS --> COMP["compiler-core
  编译器核心"]
  PKGS --> COMPD["compiler-dom
  DOM编译器"]
  PKGS --> COMPS["compiler-sfc
  单文件组件编译器"]
  PKGS --> COMPSSR["compiler-ssr
  服务端渲染编译器"]
  PKGS --> REACT["reactivity
  响应式系统"]
  PKGS --> RUNTIME["runtime-core
  运行时核心"]
  PKGS --> RUNTIMED["runtime-dom
  DOM运行时"]
  PKGS --> VUE["vue
  完整包"]
  PKGS --> TEMPC["template-compiler
  模板编译器"]
  PKGS --> SERVER["server-renderer
  服务端渲染"]
  
  %% 依赖关系
  COMP --> COMPD & COMPS & COMPSSR
  RUNTIME --> RUNTIMED
  REACT --> RUNTIME
  RUNTIMED & COMPS --> VUE
  
  %% 样式
  classDef root fill:#e1f5fe,stroke:#01579b
  classDef core fill:#e8f5e9,stroke:#2e7d32
  classDef comp fill:#fff3e0,stroke:#ff9800
  class ROOT root
  class COMP,RUNTIME,REACT core
  class COMPD,COMPS,COMPSSR,RUNTIMED,VUE,TEMPC,SERVER comp

还有服务端渲染很流行的 Nextjs 库:

flowchart TD
  ROOT[next.js] --> PF["package.json
  turbo.json"]
  ROOT --> PKGS[packages]
  ROOT --> DOCS[docs]
  ROOT --> EXAMPLES[examples]
  ROOT --> TEST[test]
  
  PKGS --> NEXT["next
  主框架包"]
  PKGS --> CREATE["create-next-app
  项目生成器"]
  PKGS --> FONT["next-font
  字体优化"]
  PKGS --> SWCE["@next/swc
  Rust编译器"]
  PKGS --> ENV["@next/env
  环境变量"]
  PKGS --> MDX["@next/mdx
  MDX支持"]
  PKGS --> BUNDLE["@next/bundle-analyzer
  打包分析"]
  
  EXAMPLES --> EX1["with-typescript"]
  EXAMPLES --> EX2["with-tailwindcss"]
  EXAMPLES --> EX3["...其他示例"]
  
  %% 依赖关系
  SWCE & ENV & FONT --> NEXT
  NEXT --> CREATE
  
  %% 样式
  classDef root fill:#e1f5fe,stroke:#01579b
  classDef core fill:#e8f5e9,stroke:#2e7d32
  classDef addon fill:#fff3e0,stroke:#ff9800
  classDef example fill:#f3e5f5,stroke:#7b1fa2
  class ROOT root
  class NEXT,SWCE core
  class CREATE,FONT,ENV,MDX,BUNDLE addon
  class EXAMPLES,EX1,EX2,EX3 example

还有很多 NodeJS 端使用的库也采用的是 workspace ,比如 nestjs 和 prisma 。

4. 手动搭建 workspace

  1. 首先准备 workspace ,我们取名叫 workspace-learn ,然后生成 package.json :
mkdir workspace-learn
cd workspace-learn
yarn init -y

然后得到了下面的文件内容:

{
  "name": "workspace-learn",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

workspace 项目必须是私有的,所以还需要添加属性:

{
  // ...其他配置
  "private": true
}

接下来新建一个文件夹 packages ,用于存放项目的位置:

mkdir packages

经过这样就得到了这样的目录结构:

workspace-learn
├── package.json
└── packages

这样 yarn 管理工具本身还不能将其识别为 workspace ,需要在 package.json 中添加:

{
	"workspace": ["packages"]
}

从这个参数我们知道,其实放置项目的位置也可以叫其他名字,比如上面我们看到有些项目有两个,一个是 packages ,另一个是 examples 。只不过普遍都使用 packages 来放置项目的位置。

  1. 将项目移动到 packages 文件夹中。我这里新增两个文件夹: onetwo 当做项目。
cd packages
mkdir one two
  1. 给项目添加依赖,假如我们添加 dayjs 。
# yarn workspace [项目名称(package.json 中的 name)] add [依赖名称]
yarn workspace one add dayjs
  1. 移除依赖 yarn workspace one remove dayjs

5. yarn 依赖合并原则

docs.npmjs.com/cli/v6/usin…

安装依赖我们可以按照连接的所有方式安装依赖。了解这个首先我们先知道版本是怎样构成的:

有三个 [major, minor, patch] ,具体可参考:semver.org/lang/zh-CN/…

  • major 主要版本号,当更新内容不兼容之前版本的时候;
  • minor 次要版本号,当新增内容兼容之前版本;
  • patch 补丁版本号,当给当前版本修复 bug ,但是不影响 API 变动。

根据这个我们知道,如果我们使用的库都是按照这个规则进行版本管理的时候,我们每次更新只要不动主要版本号就可以了。也是因为如此,我们安装第三方库的时候,默认是这样的:"dayjs": "^1.11.13",而 ^ 就代表下载软件的时候只会下载 1.11.13 <= dayjs < 2.0.0 的版本。同样 yarn 在依赖合并上也是这样做的,假如我们安装了两个版本:

# one 项目
yarn workspace one add dayjs@^1.11.13
# two 项目
yarn workspace two add dayjs@^1.11.12

安装完成后,依赖长这样:

// A 项目
{
  "dependencies": {
    "dayjs": "^1.11.12"
  }
}
// B 项目
{
  "dependencies": {
    "dayjs": "^1.11.13"
  }
}

可以看到实际安装的结果为:

如果我们不使用 ^ 那么这两个版本就不会合并,也就是

# one 项目
yarn workspace one add dayjs@1.11.13
# two 项目
yarn workspace two add dayjs@1.11.12

这个时候实际安装的结果:

也是因为这个原因,如果你的代码没有做任何修改,但是突然间项目跑不起来了,你就要从这个方面找问题。如果是很久的项目,你最先要确定的是 nodejs 版本,然后才是这个库的版本。

同时其他的方式,比如 ~>=<= 等等这些都是按照这样的规则进行合并依赖。我们说说可能常用的:

  • ^ 代表只更新次要版本和补丁版本,对于主要版本号为 0 的,则只更新补丁版本;对于主要版本和次要版本都是 0 的,则不做任何更新。参考:Caret Ranges
  • ~ 代表只更新补丁版本
  • >= 如果这样写就安装最新版本
  • 如果不使用符号,直接写版本号则代表直接安装指定版本;如果安装的时候就指定前面的,比如 dayjs@1 (类似于 dayjs@1.x )则代表安装 1.0.0 <= dayajs < 2.0.0 的版本;如果指定主要版本和次要版本则安装补丁版本最新。