概念:Monolithic vs Multi-Repo vs Mono-Repo
-
Monolithic(英 [ˌmɒnəˈlɪθɪk]) - 单体架构: 即项目通过一个repository(如git的一个仓库)进行模块或包的编码、构建、测试和发布,是目前最常见的项目代码管理模式。对于前端项目,他主要包括src/*、node-modules/*、package.json及一些工具的常用配置文件,如.gitingore、.eslintrc 等。
-
Multi-Repo 即采用多个仓库来同时管理项目代码,常见于团队协作、前后分离、模块组件化等场景。根据业务及场景需求,每一个仓库即可以独立发布项目之外,也可以作为第三方依赖。
-
Mono-Repo 是指在一个仓库中管理多个模块或包,采用这种方式管理代码的如比较知名的:create-react-app 、jest 、 webpack-cli ,也是目前比较流行的代码管理方式。
(图片来自互联网)
Git Submodules
Submodules 的产生
Submodules 工作中我们避免不了会去复用别人的模块或是代码,它可能是一个github 的 repo,也可能是隔壁团队的一个Lib. git为了应对这种场景,引入了submodule 概念。关于submodule的使用我这里不在赘述,可参考官方文档 ,有兴趣同学也可以看下
Understanding and Working with Submodules in Git
,
咱们重点说下 使用submodules的带来的管理成本及优势。
Submodules 的优势
- 跟语言或框架无关,每一个子仓库可以独立管理,有自己的依赖管理器、规则和命令,保证了父项目与子项目的独立性;
- 没有破坏性创新,可以执行所有常规的Git操作——从add、commit、pull和push没有变化,接入成本很低;
- 各项目相对独立,具备代码管理工具(如gitlab)天生的权限管理;
以上两点看似优势,其实也是短板所在,在实际的工作场景中较为突出,以下详细描述它的劣势所在。
Submodules 的缺点
-
无论 是父项目还是子项目,本质是两个不同的repo,所以每一个仓库的更新都需要单独进行处理,没有统一的管理入口,如果遇到同时几个项目一并修改,重复工作较多;
-
无法在子项目中通过 类似 get checkout 切换至特定分支,这是git本身的一种保护机制而非功能缺陷,即在submodule中,总是签出一个特定的版本而不是一个分支。如果想了解自己是哪个分支的代码,可以使用
git submodule status进行查看; -
基于第二点,当你子项目发生变更时,通过
git pull进行更新时,git 不会立刻应用到最新的代码,而需要通过git submodule update进行应用,而这恰是大家最容易忘记的点; -
管理的子项目如果相互之前存在依赖,而本身又有子项目,那么每次提交的原子性很难得到保证,甚至会一些异想不到的问题。
Submodules 本身是由 仓库管理工具,如 git 提供的一种能力,但对于一些特定领域的代码还是不太友好,如前端,还不如考虑独立维护,最后生成 NPM包供其它项目使用。正常来说,一个好的mono-repo管理工具,对于前端同学来说至少需要具备以下两种能力:
- 依赖管理:可管理所有 package 的依赖和彼此之间的关联,并将安装的依赖提升到顶层 node_modules;
- 更精准的执行和发布控制:能够进行独立或统一的测试、构建和精准发布等;
总的来说,sub modules 允许我们多元化发展(即各项目独立,可以有自己的构建工具、依赖管理策略、单元测试方法等),对于一些跨语言或跨端项目比较友好。接下来简单的了解下workspace及一些mono的管理工具,如Lerna
Workspace (yarn or npm)
Yarn在早期便提出了workapce的概念,npm 也在7.0引入(在此之前主要通过npm link实现)
本质上两者功能相仿,本文暂使用 npm workspace 来讲述workspace的使用及优势,以下将通过实际的操作演示来了解 workspace的工作原理
Workspace项目的目录结构:
(图片来自互联网)
演示
-
使用
npm init初始化整个项目,目录下生成 package.json -
分别使用
npm init -w ./packages/a和 npm init -w ./packages/b 初始化两个packages"workspaces": [ "packages/a", "packages/b" ],
-
独立给 a 安装依赖 react , 执行指令
npm install react -w a
执行上述步骤后,整体目录结构大致如下:
进入node_modules,执行 ls -l 查看文件目录,
很明显node_modules 中生成了 a 仓库的软连接,因此其它项目即可以通过常规的引用模式导入使用。另外node_modules位于整个项目的外层,即 a 和 b 共享了这个node_modules, 接下来我们执行另一个指令
- 执行指令
npm install react@16.8 -w b, 目录结构发生变化
通过上面的操作你会发现,如果安装的依赖过程中存在相同的依赖且版本号不同,则 npm为在目标项目创建一个私有的node_modules, 即通过这种方式来进行依赖版本隔离。
优势
- 提取公共依赖到top level,具有 hoist 提升功能,减少依赖的重复安装
- 私有化node_modules解决依赖冲突
- 通过软连接解决各个包相互依赖问题
Lerna - 最流行的mono-repo管理工具之一
Lerna 是最流行的mono-repo的管理工具,目前流行的Jest、webpack-cli、create-react-app都是采用Lenra进行Repo管理,网上教程较多,本文仅罗列一些特点,有想进一点了解的同学可以稳步中文官网. 其它的工具如 bolt 、Turborepo (基于go语言,并发能力强且构建基于缓存,据说比lerna快)没有实际的经验,这里不展开讨论。
配合Workspaces使用
Lerna在依赖管理上跟workspaces较为类似,它支持workspaces模式,在lerna.json 加一行配置即可:
{
...
"npmClient": "yarn",// npm
"useWorkspaces": true,
...
}
一旦设置了useWorkspaces,则lerna会放弃在lerna.json下的packages目录设置,使用package.json的packages目录配置,点击查看源码:
get packageConfigs() {
if (this.config.useWorkspaces) {
const workspaces = this.manifest.get("workspaces");
if (!workspaces) {
throw new ValidationError(
"EWORKSPACES",
dedent`
Yarn workspaces need to be defined in the root package.json.
See: https://github.com/lerna/lerna/blob/master/commands/bootstrap/README.md#--use-workspaces
`
);
}
return workspaces.packages || workspaces;
}
return this.config.packages || [Project.PACKAGE_GLOB];
}
其中 manifest,即为package.json文件内容:
get manifest() {
let manifest;
try {
const manifestLocation = path.join(this.rootPath, "package.json");
const packageJson = loadJsonFile.sync(manifestLocation);
if (!packageJson.name) {
// npm-lifecycle chokes if this is missing, so default like npm init does
packageJson.name = path.basename(path.dirname(manifestLocation));
}
// Encapsulate raw JSON in Package instance
manifest = new Package(packageJson, this.rootPath);
// redefine getter to lazy-loaded value
Object.defineProperty(this, "manifest", {
value: manifest,
});
} catch (err) {
// redecorate JSON syntax errors, avoid debug dump
if (err.name === "JSONError") {
throw new ValidationError(err.name, err.message);
}
// try again next time
}
return manifest;
}
所以设置了 "useWorkspaces": true 则意味着依赖管理完全交给 "npmClient": "yarn",// npm 你设置的npmClient,lerna本身就负责版本管理及发布。
对于版本管理,它提供了fixed及independent两种模式,前者主要是统一进行版本管理,即无论包是否更新,每次发布都会强制更新版本;后者是独立模式,有修改才会更新版本。大部分情况下,我们都采用 independent。
相对Workspaces优势
-
提供了统一的命令对包进行统一依赖管理,如使用 lerna clean 来删除所有的 node_modules;
-
两种独立的版本控制模式配合 lerna publish ,更加精细化控制版本;
总结
MonoRepo 优势比较明显,便于代码复用、简化依赖管理、团队职责分离等,局限就是权限控制较弱,存储空间会无线扩展,但瑕不掩瑜,在实际的项目中带来的益处较大,特别是前端项目。Lenra配合workspaces较好的进行依赖管理、版本控制和发布,是目前比较流行的方案之一.
MultiRepo 因为项目独立,分割在不同的仓库,使得代码模块大小较好控制,天生拥有权限控制能力,对于跨端跨语音的多团队协作是首先。
参考文档