Monorepo - 优劣、踩坑、选型 以及
一、Monorepo 介绍Monorepo 是一种项目代码管理方式指单个仓库中管理多个项目有助于简化代码共享、版本控制、构建和部署等方面的复杂性并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化这种方法已经被很多大型公司广泛使用如 Google、Facebook 和 Microsoft 等。二、Monorepo 演进阶段一单仓库巨石应用一个 Git 仓库维护着项目代码随着迭代业务复杂度的提升项目代码会变得越来越多越来越复杂大量代码构建效率也会降低最终导致了单体巨石应用这种代码管理方式称之为 Monolith。阶段二多仓库多模块应用于是将项目拆解成多个业务模块并在多个 Git 仓库管理模块解耦降低了巨石应用的复杂度每个模块都可以独立编码、测试、发版代码管理变得简化构建效率也得以提升这种代码管理方式称之为 MultiRepo。阶段三单仓库多模块应用随着业务复杂度的提升模块仓库越来越多MultiRepo这种方式虽然从业务上解耦了但增加了项目工程管理的难度随着模块仓库达到一定数量级会有几个问题跨仓库代码难共享分散在单仓库的模块依赖管理复杂底层模块升级后其他上层依赖需要及时更新否则有问题增加了构建耗时。于是将多个项目集成到一个仓库下共享工程配置同时又快捷地共享模块代码成为趋势这种代码管理方式称之为 MonoRepo。三、Monorepo 优劣场景MultiRepoMonoRepo代码可见性✅ 代码隔离研发者只需关注自己负责的仓库❌ 包管理按照各自owner划分当出现问题时需要到依赖包中进行判断并解决。✅ 一个仓库中多个相关项目很容易看到整个代码库的变化趋势更好的团队协作。❌ 增加了非owner改动代码的风险依赖管理❌ 多个仓库都有自己的 node_modules存在依赖重复安装情况占用磁盘内存大。✅ 多项目代码都在一个仓库中相同版本依赖提升到顶层只安装一次节省磁盘内存代码权限✅ 各项目单独仓库不会出现代码被误改的情况单个项目出现问题不会影响其他项目。❌ 多个项目代码都在一个仓库中没有项目粒度的权限管控一个项目出问题可能影响所有项目。开发迭代✅ 仓库体积小模块划分清晰可维护性强。❌ 多仓库来回切换编辑器及命令行项目多的话效率很低。多仓库见存在依赖时需要手动npm link操作繁琐。❌ 依赖管理不便多个依赖可能在多个仓库中存在不同版本重复安装npm link 时不同项目的依赖会存在冲突。✅ 多个项目都在一个仓库中可看到相关项目全貌编码非常方便。✅ 代码复用高方便进行代码重构。❌ 多项目在一个仓库中代码体积多大几个 Ggit clone时间较长。✅ 依赖调试方便依赖包迭代场景下借助工具自动 npm link直接使用最新版本依赖简化了操作流程。工程配置❌ 各项目构建、打包、代码校验都各自维护不一致时会导致代码差异或构建差异。✅ 多项目在一个仓库工程配置一致代码质量标准及风格也很容易一致。构建部署❌ 多个项目间存在依赖部署时需要手动到不同的仓库根据先后顺序去修改版本及进行部署操作繁琐效率低。✅ 构建性 Monorepo 工具可以配置依赖项目的构建优先级可以实现一次命令完成所有的部署。四、Monorepo 场景场景一大型项目与多项目协作场景企业或团队维护多个紧密关联的项目如前端、后端、工具库等。优势集中管理代码方便跨项目修改和协作避免代码分散导致的重复劳动。场景二共享代码与依赖场景多个项目共用组件库、工具函数或配置如 UI 组件、通用 SDK。优势直接引用内部模块避免多仓库的版本同步问题确保依赖一致性。场景三统一构建与持续集成CI/CD场景需要标准化构建、测试和部署流程。优势集中配置 CI/CD仅针对变更部分触发构建增量构建提升效率。何时谨慎使用代码量过大需要考虑构建性能、代码可维护性权限管理复杂需细化目录权限控制团队独立性高若子团队高度自治多仓库可能更灵活五、Monorepo 踩坑5.1、幽灵依赖问题npm/yarn 安装依赖时存在依赖提升某个项目使用的依赖并没有在其 package.json 中声明也可以直接使用这种现象称之为 “幽灵依赖”随着项目迭代这个依赖不再被其他项目使用不再被安装使用幽灵依赖的项目会因为无法找到依赖而报错。方案基于 npm/yarn 的 Monorepo 方案依然存在 “幽灵依赖” 问题我们可以通过 pnpm 彻底解决这个问题5.2、依赖安装耗时长问题MonoRepo 中每个项目都有自己的 package.json 依赖列表随着 MonoRepo 中依赖总数的增长每次install时耗时会较长。方案相同版本依赖提升到 Monorepo 根目录下减少冗余依赖安装使用 pnpm 按需安装及依赖缓存。5.3、构建打包耗时长问题多个项目构建任务存在依赖时往往是串行构建 或 全量构建导致构建时间较长方案增量构建而非全量构建也可以将串行构建优化成并行构建。六、Monorepo 选型6.1、构建型 Monorepo 方案此类工具主要解决大仓库 Monorepo 构建效率低的问题。项目代码仓库越来越庞大工作流int、构建、单元测试、集成测试也会越来越慢这类工具是专门针对这样的场景进行极致的性能优化。适用于包非常多、代码体积非常大的 Monorepo 项目。6.1.1、TurborepoTurborepo 是 Vercel 团队开源的高性能构建代码仓库系统允许开发者使用不同的构建系统。构建加速思路Multiple Running Task构建任务并行进行构建顺序交给开发者配置Cache、Remote Cache通过缓存 及 远程缓存减少构建时间举例 Multiple Running Task我们现在有一个 Monorepo 的项目有以下几个 packageapps/web依赖 sharedapps/docs依赖 sharedpackage/shared被 web 和 docs 依赖arduino体验AI代码助手代码解读复制代码# 当我们使用正常的 yarn workspace 去管理 monorepo 的工作流任务时例如执行以下命令 yarn workspaces run lint yarn workspaces run test yarn workspaces run build传统的 yarn workspace 问题串行构建性能差Turborepo Multiple Running Task允许用户在turbo.json中声明 task 之间依赖关系优化后构建如下举例 Local Cache第一次trubo run build后会生成缓存存放在node_modules/.cache/turbo/第一次构建示意图第二次构建示意图举例 Remote Cache想要在 CI/CD 或团队中共享打包缓存把缓存保存到了云端构建时被拉取远程缓存构建示意图6.1.2、RushRush 是微软开发的可扩展的 Monorepo 工具及解决方案。早期只提供了 Rush 作为构建调取器其余事项交给用户灵活的选择任意构建工具链由于过于灵活带来了很大的选型及维护成本后来成立了 Rush Stack 来提供了一套可复用的解决方案涵盖多项目的构建、测试、打包和发布实现了更强大的工作流。有如下工具Rush: 可扩展的 monorepo 构建编排工具Heft: 可以与 Rush 交互的可扩展构建系统API Extractor: 为工具库审阅 API 并生成 .d.ts 文件API Documenter: 生成你的 API 文档站rushstack/eslint-config: 专门为大型 TypeScript monorepo 仓库设计的 ESLint 规则集rushstack/eslint-plugin-packlets: 可用于在单个项目内来组织代码NPM 发包的一个轻量级解决方案Rundown: 用于优化 Node.js 启动时间的工具Rush 功能列举解决了幽灵依赖将项目所有依赖都安装到 Repo根目录的common/temp下通过软链接到各项目保证了 node_modules 下依赖与 package.json 一致并行构建Rush 支持并行构建多个项目提高了构建效率插件系统Rush 提供了丰富的插件系统可以扩展其功能满足不同的需求具体参考项目发布ChangeLog 支持友好自动修改项目版本号自动生成 ChangeLog6.1.3、NxNx 是 Nrwl 团队开发的同时在维护 Lerna目前 Nx 可以与 Learn 5.1及以上集成使用构建加速思路比 Turborepo 更丰富缓存通过缓存 及 远程缓存减少构建时间远程缓存Nx 公开了一个公共 API它允许您提供自己的远程缓存实现Turborepo 必须使用内置的远程缓存增量构建最小范围构建非全量构建并行构建Nx 自动分析项目的关联关系对这些任务进行排序以最大化并行性分布式构建结合 Nx Cloud您的任务将自动分布在 CI 代理中多台远程构建机器同时考虑构建顺序、最大化并行化和代理利用率分布式构建示意图用 Nx 强大的任务调度器加速 LernaLerna 擅长管理依赖关系和发布但扩展基于 Lerna 的 Monorepos 很快就会变得很痛苦因为 Lerna 很慢。这就是 Nx 的闪光点也是它可以真正加速你的 monorepo 的地方。6.2、轻量化 Monorepo 方案6.2.1、Lerna全面讲解Lerna是什么Lerna 是 Babel 为实现 Monorepo 开发的工具最擅长管理依赖关系和发布Lerna 优化了多包工作流解决了多包依赖、发版手动维护版本等问题Lerna 不提供构建、测试等任务工程能力较弱项目中往往需要基于它进行顶层能力的封装Lerna 主要做三件事为单个包或多个包运行命令 (lerna run)管理依赖项 (lerna bootstrap)发布依赖包处理版本管理并生成变更日志 (lerna publish)Lerna 能解决了什么问题代码共享调试便捷一个依赖包更新其他依赖此包的包/项目无需安装最新版本因为 Lerna 自动 Link安装依赖减少冗余多个包都使用相同版本的依赖包时Lerna 优先将依赖包安装在根目录规范版本管理Lerna 通过 Git 检测代码变动自动发版、更新版本号两种模式管理多个依赖包的版本号自动生成发版日志使用插件根据 Git Commit 记录自动生成 ChangeLogLerna 自动检测发布判断逻辑校验本地是否有没有被commit内容判断当前的分支是否正常判断当前分支是否在remote存在判断当前分支是否在lerna.json允许的allowBranch设置之中判断当前分支提交是否落后于 remoteLerna 工作模式Lerna 允许您使用两种模式来管理您的项目固定模式(Fixed)、独立模式(Independent)① 固定模式Locked modeLerna 把多个软件包当做一个整体工程每次发布所有软件包版本号统一升级版本一致无论是否修改项目初始化时lerna init默认是Locked modejson体验AI代码助手代码解读复制代码{ version: 0.0.0 }② 独立模式Independent modeLerna 单独管理每个软件包的版本号每次执行发布指令Git 检查文件变动只发版升级有调整的软件包项目初始化时lerna init --independentjson体验AI代码助手代码解读复制代码{ version: independent }Lerna 常用指令① 初始化initcsharp体验AI代码助手代码解读复制代码lerna init执行成功后目录下将会生成这样的目录结构scss体验AI代码助手代码解读复制代码- packages(目录) - lerna.json(配置文件) - package.json(工程描述文件)json体验AI代码助手代码解读复制代码{ version: 0.0.0, useWorkspaces: true, packages: [ packages/*, ], }需要在项目根目录下的package.json中设置private: truebash体验AI代码助手代码解读复制代码{ name: xxxx, version: 0.0.1, description: , main: index.js, private: true, scripts: { test: echo Error: no test specified exit 1 }, keywords: [], author: , license: ISC, devDependencies: { lerna: ^6.4.1 }, workspaces: [ packages/* ] }② 创建 packagecreatelua体验AI代码助手代码解读复制代码lerna create name [location] lerna create package1执行lerna init后默认的 lerna workspace 是packages/*需要手动修改package.json中的workspaces再执行指令生成特定目录下的 packagebash体验AI代码助手代码解读复制代码# 在 packages/pwd1 目录下生成 package2 依赖包 lerna create package2 packages/pwd1③ 给 package 添加依赖add安装的依赖如果是本地包Lerna 会自动npm link到本地包sql体验AI代码助手代码解读复制代码# 给所有包安装依赖默认作为 dependencies lerna add module-1 lerna add module-1 --dev # 作为 devDependencies lerna add module-1 --peer # 作为 peerDependencies lerna add module-1[version] --exact # 安装准确版本的依赖 lerna add module-1 --scopemodule-2 # 给指定包安装依赖 lerna add module-1 packages/prefix-* # 给前缀为 xxx 的包安装依赖④ 给所有 package 安装依赖bootstrapbash体验AI代码助手代码解读复制代码# 项目根目录下执行将安装所有依赖 lerna bootstrap执行lerna bootstrap指令会自动为每个依赖包进行npm install和npm link操作关于冗余依赖的安装npm 场景下lerna bootstrap会安装冗余依赖多个 package 共用依赖每个目录都会安装yarn 会自动 hosit 依赖包相同版本的依赖安装在根目录无需关心npm 场景下冗余依赖解决方案方案一lerna bootstrap --hoist方案二配置lerna.json/command.bootsrap.hoist true⑤ 给 package 执行 shell 指令execbash体验AI代码助手代码解读复制代码# 删除所有包内的 lib 目录 lerna exec -- rm -rf lib # 给xxx软件包删除依赖 lerna exec --scopexxx -- yarn remove yyy⑥ 给 package 执行 scripts 指令runini体验AI代码助手代码解读复制代码# 所有依赖执行 package.json 文件 scripts 中的指令 xxx lerna run xxx # 指定依赖执行 package.json 文件 scripts 中的指令 xxx lerna run --scopemy-component xxx⑦ 清除所有 package 下的依赖clean清楚所有依赖包下的 node_modules根目录下不会删除体验AI代码助手代码解读复制代码lerna clean⑧ 发布软件包自动检测publish体验AI代码助手代码解读复制代码lerna publishlerna publish做那些事儿运行lerna updated来决定哪一个包需要被publish如果有必要将会更新lerna.json中的version将所有更新过的的包中的package.json的version字段更新将所有更新过的包中的依赖更新为新版本创建一个git commit或tag将包publish到npm上⑨ 查看自上次发布的变更diff、changedbash体验AI代码助手代码解读复制代码# 查看自上次relase tag以来有修改的包的差异 lerna diff # 查看自上次relase tag以来有修改的包名 lerna changed⑩ 导入已有包importarduino体验AI代码助手代码解读复制代码lerna import [npm 包所在本地路径]⑪列出所有包list体验AI代码助手代码解读复制代码lerna list6.2.2、yarn/npm workspaceyarn 1.x 及以上版本新增 workspace 能力不借助 Lerna也可以提供原生的 Monorepo 支持需要在根目录下package.json中声明workspacejson体验AI代码助手代码解读复制代码{ private: true, // 必须是私有项目 workspaces: [project1, project2/*] }yarn workspace VS Lernayarn workspace 更突出对依赖的管理依赖提升到根目录的node_modules下安装更快体积更小Lerna 更突出工作流方面使用 Lerna 命令来优化多个包的管理如依赖发包、版本管理批量执行脚本能力及性能对比更佳方案yarn workspace Lerna如上 VS 可以看出yarn workspace 和 Lerna 各有所长yarn workspace Lerna 是更好的 Monorepo 方案执行命令yarn相当于执行lerna bootstrap即可安装所有依赖指令过渡更平滑自动依赖提升减少依赖安装。能力分工Lerna 将依赖管理交给 yarn workspaceLerna 承担依赖发布能力。操作步骤配置 Lerna 使用 Yarn 管理依赖learn.json中配置npmClient: yarn配置 Lerna 启用 Yarn Workspaces配置lerna.json/useWorkspaces true配置根目录package.json/workspaces [pacages/*], 此时 lerna.json 中的 packages 配置项将不再使用配置根目录package.json/private truescss体验AI代码助手代码解读复制代码说明 上面三个配置项需同时开启, 只开启一个 lerna 会报错 此时执行 lerna bootstrap 相当于执行yarn install等同于执行 lerna bootstrap --npm-client yarn --use-workspaces 由于 yarn 会自动 hosit 依赖包, 无需再 lerna bootstrap 时增加参数 --hoist (加了参数 lerna 也会报错)3. 不需要发包的项目配置package.json/private true6.2.3、Lerna pnpm workspacepnpm 是新一代 Node 包管理器它由 npm/yarn 衍生而来解决了 npm/yarn 内部潜在的风险并且极大提升依赖安装速度。pnpm 内部使用基于内容寻址的文件系统来管理磁盘上依赖减少依赖安装node_modules/.pnmp为虚拟存储目录该目录通过package-nameversion来实现相同模块不同版本之间隔离和复用由于它只会根据项目中的依赖生成并不存在提升。CAS 内容寻址存储是一种存储信息的方式根据内容而不是位置进行检索信息的存储方式。Virtual store 虚拟存储指向存储的链接的目录所有直接和间接依赖项都链接到此目录中项目当中的.pnpm目录pnpm 相比于 npm、yarn 的包管理器优势如下同理是 Lerna yarn workspace 优势装包速度极快缓存中有的依赖直接硬链接到项目的 node_module 中减少了 copy 的大量 IO 操作磁盘利用率极高软/硬链接方式同一版本的依赖共用一个磁盘空间不同版本依赖只额外存储 diff 内容解决了幽灵依赖node_modules 目录结构 与 package.json 依赖列表一致补充pnpm 原理存储中心 Store 集中管理依赖不同项目相同版本依赖安装只进行硬链接不同版本依赖只增加Diff文件项目package.json依赖列表和node_modules/.pnpm目录结构一致相同依赖安装时将 Store 中的依赖硬链接到项目的node_modules/.pnpm下而不是复制速度快项目node_modules中已有依赖重复安装时会被软链接到指定目录下6.4、小结如何选择6.4.1、工具对比工具TurborepoRushNxLernaPnpm Workspace依赖管理❌✅❌❌✅版本管理❌✅❌✅❌增量构建✅✅✅❌❌插件扩展✅✅✅❌❌云端缓存✅✅✅❌❌Stars20.4K4.9K17K34.3K22.7K详细对比Nx and Turborepolerna-vs-turbopack-rush6.4.2、选型建议建议采用渐进式架构方案即对于轻量级 Monorepo 项目我们初期可以选择 Lerna pnpm workspace lerna-changelog解决了依赖管理、发版管理等问题为开发者带来便利随着后续项目迭代代码变多或多个项目间依赖关系复杂可以很平滑的接入 Nx 来提升构建打包效率。