这个就是大家在网上看到的 monorepo ,其中 yarn workspace 是实现。
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
- 首先准备 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 来放置项目的位置。
- 将项目移动到
packages文件夹中。我这里新增两个文件夹:one和two当做项目。
cd packages
mkdir one two
- 给项目添加依赖,假如我们添加 dayjs 。
# yarn workspace [项目名称(package.json 中的 name)] add [依赖名称]
yarn workspace one add dayjs
- 移除依赖
yarn workspace one remove dayjs。
5. yarn 依赖合并原则
安装依赖我们可以按照连接的所有方式安装依赖。了解这个首先我们先知道版本是怎样构成的:
有三个 [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的版本;如果指定主要版本和次要版本则安装补丁版本最新。