【前端工程化】monorepo篇-rush管理monorepo实践

1,305 阅读10分钟

点赞 + 收藏 === 学会🤣🤣🤣

本文首发于本人公众号:ObjectX前端实验室

本文不会去介绍分析monorepo选型,想看选型和分析的可以去 这篇文章了解,本文的目的是讲解rush对于monorepo的实践操作,给出一个rush搭建的项目模板。

🥑 你能学到什么?

希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:

  • monorepo 演进
  • 如何使用rush管理monorepo
  • pnpm workspace 工作空间

系列文章

本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等

效果

image.png

我之前已经搭建过一个项目的了,仓库 在这里,可以先clone下来体验一下

image.png

安装一下依赖,注意这里用的是pnpm,如果没有安装自行安装一下

需要先全局安装下rush

pnpm add -g @microsoft/rush

使用pnpm全局安装如果是首次使用pnpm的话,需要设置下全局目录,使用如下命令

image.png

pnpm setup

后续会提示让你刷新bash配置,根据提示刷新一下再全局安装就行了

image.png

image.png

rush update

image.png

跑项目,start是我们用rush自定义的一个全局命令,后续会讲解模板项目是如何搭建的

rush start

image.png

一、仓库管理方式的演进

阶段描述优点缺点代码管理方式
阶段一单仓库巨石应用,一个 Git 仓库维护项目代码简单易管理,项目初期效率较高随着业务复杂度增加,代码量增多,构建效率降低,最终导致单体巨石应用Monolith
阶段二多仓库多模块应用,将项目拆分为多个业务模块,在多个 Git 仓库中管理模块解耦,复杂度降低,模块可独立编码、测试、发版,构建效率提升跨仓库代码难共享,依赖管理复杂,工程管理难度增加MultiRepo
阶段三单仓库多模块应用,将多个项目集成到一个仓库下,统一工程配置并共享模块代码跨模块代码共享方便,依赖管理简化,构建耗时减少需要管理较大的单仓库,可能带来一定的工程复杂性MonoRepo

二、rush的基本使用

简介

Rush 是由 Microsoft 开发的一个开源工具,用于管理 monorepo的构建和依赖管理。Rush 旨在解决 monorepo 项目中的常见问题,例如依赖管理复杂、构建效率低下、发布流程繁琐等。通过 Rush,可以更高效地管理大型项目,并确保团队成员之间的一致性。

概念&功能

  1. Monorepo: Rush 主要用于管理 monorepo 项目。在 monorepo 中,多个相关的项目存储在同一个代码库中,这样可以更方便地共享代码、管理依赖,并进行跨项目的变更跟踪。
  2. 项目依赖管理: Rush 提供了集中化的依赖管理方式,通过单一命令安装和管理所有项目的依赖,而不是每个项目独立管理。
  3. 版本策略: Rush 支持多个版本策略,如固定版本策略、变动版本策略等,帮助团队保持版本的一致性。
  4. 构建和发布工具链: Rush 提供了一系列工具,用于自动化构建和发布流程,支持增量构建和缓存构建结果,从而提高效率。

优点

  1. 依赖管理一致性: Rush 强制所有项目使用相同版本的依赖库,避免了依赖不一致导致的问题。它通过生成一个 shrinkwrap 文件,确保团队中所有成员使用相同的依赖版本。
  2. 构建效率高: Rush 支持增量构建和缓存机制,只对变更的部分进行重新构建,这大大提高了构建速度,特别是在大型 monorepo 中表现尤为明显。
  3. 自动化的发布流程: Rush 提供了版本控制和发布管理工具,使得发布新版本变得更加简单和可控。你可以轻松地管理多项目的版本更新和发布流程。
  4. 灵活的工作流配置: Rush 支持自定义工作流,允许开发者根据项目的需要定义不同的构建和发布步骤。
  5. 跨项目依赖管理: Rush 可以很好地处理多个项目之间的依赖关系,并确保这些依赖关系在构建和发布过程中得到正确处理。
  6. 社区支持: Rush 是由 Microsoft 维护的,拥有良好的社区支持和文档,适合大型企业使用。

缺点

  1. 学习曲线: 对于新手来说,Rush 的概念和配置可能比较复杂,需要时间来学习和掌握,特别是在与其他工具(如 Yarn、Lerna 等)对比时。
  2. 初始配置复杂: 初次设置 Rush 时,可能需要进行较多的配置工作,尤其是对于现有项目迁移到 Rush 的过程中,配置可能会比较繁琐。
  3. 生态系统限制: 虽然 Rush 兼容 npm、Yarn、PNPM 等工具,但在某些情况下,可能会遇到与其他工具或插件不兼容的问题。
  4. 特定场景适用性: Rush 主要面向大型 monorepo 项目管理,对于小型或独立项目,可能会显得过于复杂和笨重。
  5. 依赖于缓存机制: Rush 的性能提升部分依赖于其缓存机制,尽管这通常是一个优点,但在某些情况下,缓存可能会导致问题,例如缓存不一致或缓存失效。

总结

Rush 的概念和配置可能比较复杂,需要时间来学习和掌握,很多理念我们可能用不到,实践中我们仅用到了rush管理多包,rush自定义命令,这两个足以。

三、实践-实现一个rush管理的monorepo仓库模板

使用rush初始化

rush init

image.png

它将会生成以下文件:(更多信息可查阅 配置文件参考

文件用途
rush.jsonRush 内主要的配置文件
.gitattributes(如果你不用 Git 可以删除) 告诉 Git 不要对哪些 shrinkwrap 文件进行合并,因为该操作并不安全
.gitignore(如果你不用 Git 可以删除) 告知 Git 不要跟踪哪些文件
.travis.yml(如果你不用 Travis 可以删除) 配置 Travis CI 服务来在 PR 中使用 Rush
common/config/rush/.npmrcRush 用该文件配置源,无论是 PNPM, NPM 或者 Yarn
common/config/rush/command-line.json用于自定义 Rush 的命令行命令或参数
common/config/rush/common-versions.json用于指定 NPM 包的版本,它影响 Rush 仓库下所有项目
common/config/rush/pnpmfile.js(如果不使用 PNPM 可以删除) 用于解决 package.json 文件下错误的依赖关系
common/config/rush/version-policies.json用于定义发布配置

注意: 如果你的分支中已经存在这些文件,rush init 将会发出警告并且不会覆盖已有的文件。版本不一致可能有一些不一致,问题不大,大多数不用关注

git初始化一下

保存下,方便后面出问题回退

image.png

增加项目

创建projects目录,可以用vite新建项目,或者搬已有的项目,模板仓库里面的都是我用vite创建,改的,我就直接复制过来了,一个test-core,一个test-ui,配置的话看过之前的文章应该都能理解,我们这里只讲解rush的配置

image.png

更改rush.json的projects为

"projects": [
    {
      "packageName": "test-ui",
      "projectFolder": "projects/test-ui"
    },
    {
      "packageName": "test-core",
      "projectFolder": "projects/test-core"
    }
  ]

两个项目的依赖关系是test-ui使用了test-core,我们需要在test-ui的package.json中加上工作空间

"dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "test-core": "workspace:*"
  },

然后rush update首次初始化安装下依赖,你将会发现 rush update 创建了一些新文件:

  • common/scripts/install-run-rush.js: 用于在 CI 任务中以一种可靠的方式来启动 Rush
  • common/scripts/install-run.js: 用于在 CI 任务中以一种可靠的方式来启动任意工具

image.png

rush build构建一下,查看rush是否接管成功

image.png

autoinstaller: 编写自定义启动脚本

如何自定义命令

common/config/rush/command-line.json 下有一个配置文件用于自定义指令和参数,你的配置文件应该满足 command-line.schema.json 范式.

自定义指令: 你可以像 Rush 内置的指令(例如 rush buildrush check 等)一样自定义自己的指令,这有两种类型:

  • bulk: bulk 类型的指令会在每个项目中被调用,其原理与 rush build. 设定 "enableParallelism": true 后,项目可以并行运行。
  • global: global 类型的指令会在整个仓库内执行指定的脚本文件。

你也可以自定义命令行“参数”。一个参数可以通过 associatedCommands 来被一个或多个指令关联。你甚至可以将自定义参数关联到 Rush 内置的 build 和 rebuild 指令上。在上述示例中,我们将 --ship 参数关联到 rush buildrush rebuild 和自定义的 rush import-strings 上。

目前有三种 parameterKind 类型:

  • flagflag 是一个布尔属性的参数,例如 --ship.
  • choicechoice 是一个从列表选择的额外参数,例如 --locale fr-fr.
  • stringstring 是一个可以接受任何字符串值的参数,例如 --name my-new-package.

修改command-line.json

如下增加自定义命令,parameters暂时不用管,后续文章会讲解

{
  "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",

  "commands": [
    {
      "commandKind": "global",
      "name": "start",
      "summary": "启动项目",
      "description": "启动项目",
      "safeForSimultaneousRushProcesses": true,
      "shellCommand": "path脚本路径",
      // 自动安装器名称,不写这个,脚本的依赖不会自动安装
      "autoinstallerName": "script-cli"
    }
  ],

  "parameters": [
    
  ]
}

增加自动安装器

脚本都写着新建的这个安装器下面,使用此命令后common目录下会新增一个autoinstallers目录,common/autoinstallers/script-cli

rush init-autoinstaller --name script-cli

image.png

image.png

增加脚本

增加utils.jsrushxRush 工具中的一个命令,用于执行在项目的 package.json 文件中定义的脚本中命令

const path = require("path");
// 引入 Node.js 的 path 模块,用于处理文件和目录路径

const shell = require("shelljs");
// 引入 shelljs 模块,用于在 Node.js 中执行 shell 命令

/**
 * 在项目 projectName 中执行 command
 * @param {string} projectName - 项目名称
 * @param {string} command - 要在项目中执行的命令
 * @param {function} cb - 可选的回调函数,在命令执行完成后调用
 */
function runProject(projectName, command, cb) {
  // 使用 path.join 将项目路径拼接成一个完整的路径
  const projectPath = path.join(__dirname, `../../../projects/${projectName}`);

  // 使用 shell.exec 执行命令,将目录切换到项目路径并执行指定命令
  shell.exec(
    `cd ${projectPath} && rushx ${command}`,
    (code, stdout, stderr) => {
      // 检查命令的退出码,如果不为 0,表示命令执行出错
      if (code !== 0) {
        console.error(projectName, "执行命令出错:", stderr);
        // 输出错误信息并退出当前进程
        process.exit(1);
      }

      // 如果提供了回调函数,则执行回调函数
      cb && cb();
    }
  );
}

/**
 * 检查dist是否存在,如果不存在,需要先 build,之后再执行 command
 * @param {Array} projectPaths - [打包顺序数组]
 * @param {function} cb - 回调函数,在检查和构建完成后调用
 */
function preStart(projectPaths, cb) {
  // 遍历传入的项目路径
  projectPaths.forEach((projectPath) => {
    // 将每个项目路径拼接成完整路径
    const completePath = path.join(__dirname, `../../../${projectPath}`);

    // dist 目录不存在
    if (!shell.test("-d", `${completePath}/dist`)) {
      // 输出提示信息,表示正在打包
      console.log(`${projectPath} 不存在,正在打包中...`);

      // 执行打包命令,使用 rushx build
      const buildResult = shell.exec(`cd ${completePath} && rushx build`);

      // 检查构建是否成功
      if (buildResult.code !== 0) {
        // 如果构建失败,输出错误信息并删除 dist 目录
        console.error(`打包 ${projectPath} 失败,正在退出...`);
        shell.exec(`rm -rf ${completePath}/dist`);
        // 退出当前进程
        process.exit(1);
      }
    }
  });

  // 构建完成后,调用回调函数
  cb();
}

module.exports = {
  // 导出 runProject 和 preStart 函数供外部使用
  runProject,
  preStart,
};

增加start.js,内容如下

const { preStart, runProject } = require("./utils");

preStart(["projects/test-core", "projects/test-ui"], () => {
  runProject("test-ui", "start");
});

启动项目

这样就达到了我们之前想要的效果

rush start

image.png

image.png

总结

本篇文章我们只是简单的实现了一个用rush管理的monorepo项目,但是很多配置,比如代码风格统一,代码质量检测,commitlint什么的我们还没有去实现,但是rush也是支持的,后续我们会将组件库项目和一个手写的博客项目改为monorepo,用rush管理会去实现其他的东西

待办

  • monorepo发包脚本
  • 组件库项目&博客项目搬家
  • rush代码风格、代码质量管理
  • ...