记一次项目 monorepo 架构改造

226 阅读5分钟

前言

公司项目很多,各个项目之间关联性较强,开发时需要在各个项目中来回切换,打包构建不统一,不自动,需要人工判断各个项目的打包顺序,项目间共同代码不能复用,代码规范不统一等等不方便的问题,于是想到了使用 monorepo 模式对现有系统进行改造。

于是问题来了,怎么改?

怎么做

在写这里的时候突然想到了一个问题的答案,这个问题就是如何把大象装进冰箱里?

答案很简单:

  • 打开冰箱
  • 把大象放进去
  • 关上冰箱

在这个项目里,对应的答案也很简单。

  • 创建一个新仓库
  • 复制需要改造的项目到新仓库
  • 上传新仓库

现在这个项目不就是 monorepo 了嘛。。。

开个玩笑。。。

要实现 monorepo 架构,我们首先要进行技术选型,这次项目就选用 pnpm + turbo。

为了尽量不影响正常业务开发,我们将整个执行阶段分为三个。

  • 代码集中阶段
  • 具体实现阶段
  • 优化阶段

代码集中

为了集中代码,我们需要先创建一个单独的仓库,并单独创建一个新的项目。这个项目可以使用 turbo 官方的 cli 工具进行创建,这样我们就有了官方的项目架构。

// 创建 turbo 项目
npx create-turbo@latest
// 官方项目架构
- 📁 my-turborepo
  - 📁 apps // 存放具体业务项目
  - 📁 packages // 存放公共逻辑,如组件库、工具函数、配置信息等
  - 📄 package.json
  - 📄 pnpm-lock.yaml
  - 📄 pnpm-workspace.yaml // 定义工作区,工作区下的所有子目录作为独立的包
  - 📄 turbo.json
  - ...

这时我们得到了一个基础项目,接下来删除这个项目中的具体示例代码,将我们的各个项目移入 apps 目录下,组件库移入packages 目录下。

为了方便管理和语义化,将各个包中的名字加上统一前缀。如 @repo。

这个时候,在各个仓库下,原来的打包编译功能是不受影响的因为只是将代码复制了一份放到另一个位置。

接下来,在统一名字的基础上,统一命令,这里简单化为三条命令:

  • dev
  • build
  • lint

这是为了 turbo 调用每个子包的命令。后面配置 turbo 再具体说明。

具体实现

在这个阶段主要做这几件事:

  • 公共代码抽离
  • CICD 重构
  • 依赖关系疏理

公共代码抽离

在 monorepo 架构中,由于多个项目在一个仓库中,所以可以很方便的实现代码复用,于是可以将各个项目的公共代码抽离到单独的包,避免重复工作。例如工具函数,业务组件等。

同时,为了统一代码风格,规范项目配置,对于一些配置文件,比如 eslint 配置,tsconfig 配置等,也可以单独抽离成第三方包。

依赖关系疏离

在 pnpm + turbo 项目中,会自动根据 package.json 文件中的依赖进行梳理。

// 通过 workspace 引入依赖
// 根据这个配置,turbo 会知道当前包依赖哪些包,从而确定打包顺序,也可以在 turbo.json 中手动处理。
"dependencies": {
    "@demo/ui": "workspace:*",
    "@demo/utils": "workspace:*",
    "@demo/ts-config": "workspace:*",
},

重构 CICD

之前的项目打包方式没有改变,所以并不能全自动处理一些命令的执行,于是需要通过 turbo 来执行对应的命令。

{
    "$schema": "https://turbo.build/schema.json",
    "tasks": {
        "build:watch": {
            "cache": false,
            "persistent": true
        },
        "build": {
            "dependsOn": ["^build"], // 按依赖关系,先打包被依赖的包
            "outputs": []
        },
        "dev": {
            "cache": false,
            "persistent": true
        },
        "typecheck": {
            "cache": false,
            "persistent": true
        }
    },
    "ui": "tui",
    "cacheDir": ".turbo/cache"
}

优化阶段

到了这个阶段,项目已经可以正常运行开发了。

但是也会有一些需要解决的问题:

  • 权限问题
  • 构建发布流程

权限问题

在单仓库应用中,会给予对应的开发人员该仓库的权限,一般是谁需要做某个需求,就会将该需求涉及到的仓库开发权限授予该开发人员。但是在这个项目中,需要对权限重新设计,使用权限组来管理。

针对需要分割权限的项目,单独设置权限组,将对应人员加入该权限组。

对权限的管理需要自动化配合制度指定。

可以使用 gitlab 群组功能,配合 CODEOWNERS 文件限制开发者对分支的合并和提交。

# Frontend 代码只能由 frontend-team 审核和合并
packages/frontend/ @frontend-team

# Backend 代码只能由 backend-team 审核和合并
packages/backend/ @backend-team

# CI/CD 配置只能由 DevOps 处理
.gitlab-ci.yml @devops-team

为了限制开发者修改提交不属于自己负责的代码,可以使用 husky 进行 pre-commit 校验。

#!/bin/bash
// 获取当前用户权限
// 获取当前修改的项目
// 校验权限

还有就是配合制度约束,每个项目的发布流程需要按公司情况具体制定,这里就不多说。

构建发布流程

构建发布流程在公司内部主要是制度上和流程上的约束,因为发布基本都是自动化的。这一点其实和普通项目区别不大,只是需要修改下脚本命令,使用 turbo。

主要问题是,在修改底层包时,是否需要自动打包上层包,在这个项目是不,解决方案是在这个底层包提交合并时,会通知上层依赖包的开发人员,从而统一修改发布。

// 获取依赖当前包的包
grep -B 5 "@my-monorepo/utils" pnpm-lock.yaml

结束

这个项目就改造完成了。。。