Skip to content

19.建立组件库生态:利用 Monorepo 方式管理组件库生态

2246字约7分钟

2024-10-28

组件库一般都会配有周边产品,比如 Admin 、Template、CLI 工具等等。周边产品相当于有关联的多个项目,更准确的说法是多个软件包。这个时候就应该使用 Monorepo 方式组织代码,方便频繁在多个项目间同时交替开发,同时发布,保持版本间没有冲突。

传统 Mutirepo 方式的不足

所谓传统方式,我们称之为 Multirepo 方式,或者可以称之为 MutiPackage-MultiRepo 方式。就是遇到多个软件包的场景,使用多个 Repo 仓库的方式组织代码。

换句话说就是,一个软件包一个 Repo 仓库。其实我们常见的前端项目默认就是这样的模式。这种方式最大的问题就是在多个项目间切换开发会非常不方便。比如: 在开发 Admin 项目的时候,发现 UI 库需要增加了一个功能,那你需要做以下步骤:

  • 从 Git 库克隆 UI 库代码;
  • 修改 UI 库代码;
  • 推送 UI 库到 Git 库;
  • 推送 UI 库到 Npm 库;
  • 在 Admin 中更新最新的 UI 库;

这个过程假设一次修改不满意频繁更新,那么整个过程还会不断重复。

优化的方案,是使用 npm link 方式把几个项目的本地目录链接起来。但是这种方法依然有弊端,比如在团队开发的时候,你必须随时同步所有的代码仓库。另外如果你的代码不希望公开到 Npm 上,你还需要建立私有的 Npm 仓库。

Monorepo 的优势

Monorepo 其实就是将多个项目 (pacakage 软件包)放到同一个仓库 (Repo) 中进行管理。这种代码组织形式可以更好地管理多 Package 项目。主要的优点有:

  • 可见性 (Visibility): 每个开发者都可以方便地查看多个包的代码,方便修改跨 Package 的 Bug。比如开发 Admin 的时候发现 UI 有问题,随手就可以修改。
  • 更简单的包管理方式(Simpler dependency management): 由于共享依赖简单,因此所有模块都托管在同一个存储库中,因此都不需要私有包管理器。
  • 唯一依赖源(Single source of truth): 每个依赖只有一个版本,可以防止版本冲突,没有依赖地狱。
  • 原子提交: 方便大规模重构,开发者可以一次提交多个包(package)。

同样是上面的那个同时开发 Admin 和 UI 的场景。当你开发 Admin 时,发现 UI 有要修改之处,只需要切换目录修改,这时候马上就可以验证修改后的效果了,无需提交软件包,无需担心软件冲突。

越复杂的场景,你会发现这种好处会更加明显。比如一个 UI 库对应两个 Admin 。这时候你希望重构一下某个组件的属性。这种重构需要同时调整三个包中的代码。使用 Monorepo ,可以不必有任何负担,调整后立刻验证两个 Admin 效果,同时发布就好。

用户故事 (UserStory)

将组件库重构为 Monorepo 风格管理(包含组件库、文档、CLI 工具三个子项目),方便后续组件库生态建设。

任务分解 (Task)

  • Monorepo 方案选型;
  • 重构 Monorepo;
  • 测试 Monorepo 效果;

方案选型

目前 JS 中常见的 Monorepo 大概有两种选择:Lerna、Pnpm workspace。

其实在 Pnpm 横空出世前,基本上就是 lerna 一统天下的局面。连 Vue3 早期都是使用 lerna 做的 Monorepo 方案。

lernaJS 是由 Babel 团队编写的多包管理工具。因为 Babel 体系的规模庞大后有很多子包需要管理,放在多个仓库管理起来比较困难。

https://github.com/babel/babel/tree/main/packages

包多到一眼望不到边

长江后浪推前浪,2021 年底 pnpm 横空出世,闪电般的性能一下子征服了所有前端开发者。更重要的是它还附带 monorepo 方案。这个时候基本上没有任何开发者会抵挡这种诱惑,包括 Vue3.0。

毫无疑问,我们选择 pnpm 来搭建。

提示

接下来要将前面搭建的组件库项目改成 monorepo 结构,可以先 打个 tag 推送到 github 保存当前版本,也可以切分支来修改,或者建个新项目。

修改软件包目录结构

├── packages
|   ├── create-cli  // CLI工具
|   |   ├── package.json
|   ├── ui  // UI组件库
|   |   ├── package.json
|   ├── docs // docs文档
|   |   ├── package.json
├── package.json
├── pnpm-workspace.yaml

然后需要在软件包中禁用 npm 和 yarn,这么做的目的是允许项目使用 pnpm 进行模块管理。不然的话会出现不兼容问题。

方法是添加 preinstall npm hook 钩子,这个钩子会在安装模块前触发,检查该代码是否是使用 pnpm 运行。如果不是的话会推出并提示错误。

在 package.json 中添加脚本:

{
  "scripts": {
    "preinstall": "npx only-allow pnpm"
  }
}

当运行 npm installyarn,就会发生错误并且不会继续安装。

只有 pnpm install才可以

初始化工作空间

在 monorepo 项目中,每个软件包都会存放在工作空间,方便管理。

首先在根目录创建 pnpm-workspace.yaml,这个文件用于声明所有软件包都要存放在 packages 目录之中。其实目前 monorepo 风格的项目也普遍使用 packages 作为默认软件包目录位置。

packages:
  # all packages in subdirs of packages/ and components/
  - "packages/**"

Example

提示

文档不想继续写了,参考Project A or B,自行修改成 monorepo 项目,可以参考我改造后的仓库结构和 package.json 等文件。 (是 TMD 让你参考,不是抄答案连名字都一起抄!!!)

项目结构

pnpm-workspace.yaml
packages:
  - "packages/*"

catalog:
  vite: ^5.4.9
  typescript: ^5.6.2
  vue: ^3.5.12
  unocss: ^0.63.6
  "@vitejs/plugin-vue-jsx": ^4.0.1

修改发布工作流

这里用递归对子项目发包到 npm,只发布未设置 "private": true 的项目。

name: PublishToNpm

on:
  push:
    # tags:
    #   - 'v*' # 推送 tag 为 v 开头时触发,比下面的推送到分支触发更正规些,自己选择
    branches: [main]

jobs:
  publish:
    permissions:
      id-token: write
      contents: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Set node
        uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: pnpm
          registry-url: "https://registry.npmjs.org"

      - name: Install Dependencies
        run: pnpm i

      - name: PNPM build
        run: pnpm run build

      - name: copy readme
        run: cp README.md ./packages/ui/

      - name: Publish to NPM
        run: pnpm -r publish --access public --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
          NPM_CONFIG_PROVENANCE: false

之前的 main.yml 工作流也可以优化下,去掉一些重复的步骤。

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  LINT_AND_BUILD:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      - uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Set node
        uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: pnpm
          registry-url: "https://registry.npmjs.org"

      - name: Install modules
        run: pnpm install

      - name: Run Lint
        run: pnpm lint

      - name: Run Build
        run: pnpm build