一、什么是 MonorepoMonorepoMonolithic Repository单一仓库是一种代码管理策略将多个相关的项目或包放在同一个 Git 仓库中进行管理。与之对应的是Polyrepo多仓库即每个项目各自拥有独立的 Git 仓库。以本项目为例我们的仓库结构如下low-code-platform/ # 根目录单一仓库 ├── package.json # 根配置声明 workspaces ├── packages/ │ ├── shared/ # 共享类型和工具库 │ │ ├── package.json # low-code/shared │ │ └── src/ │ ├── backend/ # 后端服务 │ │ ├── package.json # low-code/backend │ │ └── src/ │ └── frontend/ # 前端应用 │ ├── package.json # low-code/frontend │ └── src/ └── node_modules/ # 统一的依赖目录三个独立的包shared、backend、frontend一个仓库管理。这就是 Monorepo。二、Monorepo 的作用2.1 代码共享shared包中定义的类型和工具backend和frontend可以直接import引用不需要发布 npm 包再安装。// packages/backend/src/services/page.service.ts// 直接引用 shared 包的类型无需发布import{Page,Component}fromlow-code/shared;2.2 统一依赖管理根目录一个package.json统一管理公共依赖和脚本避免各子包之间出现版本不一致的问题。比如typescript在三个子包中都是^5.3.0统一管控。2.3 原子提交一个 PR 可以同时修改后端接口和前端调用代码保证它们始终匹配。不会出现后端改了接口但前端还没更新的情况。2.4 统一 CI/CD一个Jenkinsfile或 CI 配置文件就能搞定所有子包的构建、测试和部署流程。三、Monorepo 解决什么问题3.1 跨仓库同步难Polyrepo 的痛点改了shared的类型定义需要先发包再去backend和frontend仓库分别升级版本。Monorepo 的方案改一处到处生效。内部包通过 workspace 链接修改立即可见。3.2 依赖版本地狱Polyrepo 的痛点多个仓库各自安装依赖容易出现 A 仓库用 React 18、B 仓库用 React 17 的混乱局面。Monorepo 的方案依赖统一提升到根目录的node_modules相同依赖共享同一版本。3.3 代码复用成本高Polyrepo 的痛点共享代码需要走发 npm 包 → 安装 → 更新的完整流程。Monorepo 的方案直接import就行开发体验和在同一个项目里写代码一样。3.4 开发体验碎片化Polyrepo 的痛点在多个仓库之间来回切换分别安装依赖分别跑构建。Monorepo 的方案一个仓库一次npm install统一的脚本入口。四、Monorepo 中的子包可以单独发布吗完全可以。这也是 Monorepo 的一大优势——开发在一起发布可以独立。每个子包都有自己的package.json本身就是一个完整的 npm 包packages/shared→ 发布为low-code/sharedpackages/backend→ 发布为low-code/backendpackages/frontend→ 通常构建后部署不一定需要发 npm 包4.1 发布配置示例{name:low-code/shared,version:1.0.0,main:dist/index.js,types:dist/index.d.ts,files:[dist],scripts:{build:tsc}}{name:low-code/backend,version:1.0.0,description:低代码平台后端服务 - 使用JSON文件存储,main:src/index.ts,scripts:{dev:ts-node-dev --respawn --transpile-only src/index.ts,build:tsc,start:node dist/index.js},dependencies:{express:^4.18.2,cors:^2.8.5,bcryptjs:^2.4.3,jsonwebtoken:^9.0.2,uuid:^9.0.0},devDependencies:{types/express:^4.17.21,types/cors:^2.8.17,types/bcryptjs:^2.4.6,types/jsonwebtoken:^9.0.5,types/uuid:^9.0.7,types/node:^20.10.0,typescript:^5.3.0,ts-node-dev:^2.0.0}}4.2 内部引用 vs 发布后引用开发时子包之间通过 workspace 协议引用{dependencies:{low-code/shared:workspace:*}}workspace:*指向本地包。发布时工具会自动将其替换为真实版本号如^1.0.0从 npm registry 安装。4.3 推荐的发布工具Changesets自动检测变更、管理版本号、批量发布Lerna经典的 Monorepo 发布工具Turborepo侧重构建缓存和任务调度也支持发布流程五、从零创建本项目 Monorepo 的完整步骤第一步初始化根目录mkdirlow-code-platformcdlow-code-platformgitinitnpminit-y编辑根目录package.json{name:low-code-platform,version:1.0.0,description:低代码平台 - 使用JSON文件存储,private:true,workspaces:[packages/shared,packages/backend,packages/frontend],scripts:{dev:backend:cd packages/backend npm run dev,dev:frontend:cd packages/frontend npm run dev,build:shared:cd packages/shared npm run build}}04关键点private: true是必须的防止根目录被意外发布到 npmworkspaces数组声明哪些目录是子包第二步创建子包目录结构mkdir-ppackages/shared/srcmkdir-ppackages/backend/srcmkdir-ppackages/frontend/src第三步初始化每个子包分别在每个子包目录下创建package.jsoncdpackages/sharednpminit-ycd../backendnpminit-ycd../frontendnpminit-ycd../..然后修改每个子包的name使用统一的 scope 前缀low-code/packages/shared/package.json{name:low-code/shared,version:1.0.0,description:低代码平台共享类型和工具,main:src/index.ts}packages/backend/package.json{name:low-code/backend,version:1.0.0,description:低代码平台后端服务 - 使用JSON文件存储,main:src/index.ts}packages/frontend/package.json{name:low-code/frontend,version:1.0.0,description:低代码平台前端应用,type:module}第四步安装各子包的依赖在根目录执行使用-w参数指定目标子包# shared 包npminstall-Dtypescript-wpackages/shared# backend 包npminstallexpress cors bcryptjs jsonwebtoken uuid-wpackages/backendnpminstall-Dtypescript ts-node-dev types/express types/cors types/bcryptjs types/jsonwebtoken types/uuid types/node-wpackages/backend# frontend 包npminstallreact react-dom react-router-dom axios-wpackages/frontendnpminstall-Dtypescript vite vitejs/plugin-react types/react types/react-dom-wpackages/frontendnpm workspaces 会自动将依赖提升hoist到根目录的node_modules子包之间共享相同版本减少重复安装。第五步建立子包之间的引用关系让backend和frontend引用shared包npminstalllow-code/shared-wpackages/backendnpminstalllow-code/shared-wpackages/frontendnpm 会自动识别这是 workspace 内部的包创建符号链接symlink而不是从 registry 下载。第六步配置 TypeScript每个子包创建各自的tsconfig.json。第七步添加根目录统一脚本{scripts:{dev:backend:cd packages/backend npm run dev,dev:frontend:cd packages/frontend npm run dev,build:shared:cd packages/shared npm run build}}第八步安装所有依赖npminstall在根目录执行一次npm 会扫描所有 workspace统一安装依赖处理好内部包的链接关系。六、常见的 Monorepo 工具对比工具特点适用场景npm workspacesNode.js 内置零配置中小型项目简单需求Yarn workspaces成熟稳定社区广泛中大型项目pnpm workspaces磁盘空间节省安装速度快对性能有要求的项目Turborepo智能构建缓存任务调度构建频繁的大型项目Nx功能全面依赖图分析企业级大型项目Lerna经典工具版本管理和发布需要频繁发包的项目本项目使用的是npm workspaces是最轻量的方案适合当前的项目规模。七 monorepo VS 微前端 VS npm发包1.Monorepo 多个项目放在一个仓库里通过 workspaces 共享代码。优点代码共享零成本改了公共包立刻生效不需要发版、不需要更新版本号统一的依赖管理避免不同项目用不同版本的同一个库导致的冲突原子提交一个 PR 可以同时改前端、后端、公共包保证一致性重构方便IDE 可以跨包跳转和重命名缺点仓库会越来越大clone 和 CI 构建变慢权限管理粗粒度所有人都能看到所有代码虽然可以用 CODEOWNERS 缓解构建工具配置复杂需要处理包之间的依赖关系和构建顺序所有项目耦合在一起一个包的 CI 挂了可能阻塞其他包的发布适合场景同一个团队维护的紧密关联的项目2.npm 发包后引入 公共代码发布到 npm或私有 registry其他项目通过 npm install 安装使用。优点版本管理清晰每个消费方可以锁定自己需要的版本升级节奏自主可控项目完全解耦各自独立仓库、独立 CI/CD适合跨团队共享不需要在同一个仓库里仓库体积小职责单一缺点开发调试麻烦改了公共包要先发版消费方再更新版本号才能用到新代码虽然可以用 npm link 缓解但体验一般版本碎片化不同项目可能用着不同版本的公共包容易出现兼容性问题发布流程有额外成本需要维护 changelog、语义化版本号等跨包重构困难改了公共包的接口不知道哪些消费方会受影响适合场景通用工具库、组件库需要被多个不相关的项目使用比如公司内部的 UI 组件库。3.微前端qiankun、Module Federation、single-spa 等多个独立前端应用在运行时组合成一个整体每个子应用可以独立开发、独立部署。优点技术栈无关子应用 A 用 React子应用 B 用 Vue完全没问题独立部署改了一个子应用只需要部署那一个不影响其他团队自治不同团队负责不同子应用互不干扰渐进式迁移老项目可以一块一块地迁到新技术栈缺点运行时开销需要加载框架的运行时、处理沙箱隔离、样式隔离等复杂度高子应用之间的通信、状态共享、路由协调都需要额外处理用户体验可能受影响子应用切换时可能有加载延迟公共依赖重复加载虽然可以通过 externals 或 shared 优化但配置复杂调试困难问题可能出在主应用、子应用、或者它们之间的交互适合场景大型企业级应用多个团队协作或者需要整合不同技术栈的遗留系统。QAQ1.针对monorepo得仓库而言其根目录是不是一定要有package.json文件A1.取决于你用的包管理工具。如果是npm/yarn workspaces则必须有。为 workspaces 的声明就写在根目录的 package.json 里没有它包管理器就不知道哪些子目录是 workspace // 根目录 package.json { workspaces: [ packages/shared, packages/backend, packages/frontend ] } 对于其他得例如pnpm则不一定但是需要 pnpm-workspace.yaml。Q2.项目 a 没有部署项目 b 和 c 还能用 a 的东西吗A2.这里要区分两个概念部署和安装依赖 部署是指把项目发布到线上运行这跟项目之间的代码引用没关系。 项目 b 和 c 引用项目 a 的东西发生在开发阶段和构建阶段。在构建build的时候a 的代码会被打包进 b 和 c 的产物里。所以最终 b 和 c 各自的部署包已经包含了 a 的代码a 本身不需要单独部署。 以当前得monorepo项目为例low-code/shared 这个包不会单独部署它的类型定义在构建时就被前端和后端各自打包进去了。Q3.是不是只要多个项目在同一个文件夹下就能互相引用不是光放在一起不够。需要做额外的配置才能让它们互相识别。常见的方式有npm/yarn/pnpm workspaces — 在根目录的 package.json 里声明 workspaces包管理器会自动把本地包链接起来。 // 根目录 package.json { workspaces: [packages/*] } 然后在 b 和 c 的 package.json 里声明依赖 { dependencies: { low-code/shared: * } } 这样包管理器在 npm install 时会把本地的 low-code/shared 通过 symlink符号链接链接到 b 和 c 的 node_modules 里而不是去 npm 仓库下载。、总结Monorepo 不是银弹但对于像本项目这样前后端 共享库的多包结构它提供了开发效率代码共享零成本修改即时生效一致性保障依赖版本统一接口变更原子提交灵活的发布策略开发在一起发布可以独立简化的工程配置一套 CI/CD一次依赖安装核心思路就是用一个仓库的便利性管理多个包的独立性。
保姆级从0到1创建属于自己得monorepo
一、什么是 MonorepoMonorepoMonolithic Repository单一仓库是一种代码管理策略将多个相关的项目或包放在同一个 Git 仓库中进行管理。与之对应的是Polyrepo多仓库即每个项目各自拥有独立的 Git 仓库。以本项目为例我们的仓库结构如下low-code-platform/ # 根目录单一仓库 ├── package.json # 根配置声明 workspaces ├── packages/ │ ├── shared/ # 共享类型和工具库 │ │ ├── package.json # low-code/shared │ │ └── src/ │ ├── backend/ # 后端服务 │ │ ├── package.json # low-code/backend │ │ └── src/ │ └── frontend/ # 前端应用 │ ├── package.json # low-code/frontend │ └── src/ └── node_modules/ # 统一的依赖目录三个独立的包shared、backend、frontend一个仓库管理。这就是 Monorepo。二、Monorepo 的作用2.1 代码共享shared包中定义的类型和工具backend和frontend可以直接import引用不需要发布 npm 包再安装。// packages/backend/src/services/page.service.ts// 直接引用 shared 包的类型无需发布import{Page,Component}fromlow-code/shared;2.2 统一依赖管理根目录一个package.json统一管理公共依赖和脚本避免各子包之间出现版本不一致的问题。比如typescript在三个子包中都是^5.3.0统一管控。2.3 原子提交一个 PR 可以同时修改后端接口和前端调用代码保证它们始终匹配。不会出现后端改了接口但前端还没更新的情况。2.4 统一 CI/CD一个Jenkinsfile或 CI 配置文件就能搞定所有子包的构建、测试和部署流程。三、Monorepo 解决什么问题3.1 跨仓库同步难Polyrepo 的痛点改了shared的类型定义需要先发包再去backend和frontend仓库分别升级版本。Monorepo 的方案改一处到处生效。内部包通过 workspace 链接修改立即可见。3.2 依赖版本地狱Polyrepo 的痛点多个仓库各自安装依赖容易出现 A 仓库用 React 18、B 仓库用 React 17 的混乱局面。Monorepo 的方案依赖统一提升到根目录的node_modules相同依赖共享同一版本。3.3 代码复用成本高Polyrepo 的痛点共享代码需要走发 npm 包 → 安装 → 更新的完整流程。Monorepo 的方案直接import就行开发体验和在同一个项目里写代码一样。3.4 开发体验碎片化Polyrepo 的痛点在多个仓库之间来回切换分别安装依赖分别跑构建。Monorepo 的方案一个仓库一次npm install统一的脚本入口。四、Monorepo 中的子包可以单独发布吗完全可以。这也是 Monorepo 的一大优势——开发在一起发布可以独立。每个子包都有自己的package.json本身就是一个完整的 npm 包packages/shared→ 发布为low-code/sharedpackages/backend→ 发布为low-code/backendpackages/frontend→ 通常构建后部署不一定需要发 npm 包4.1 发布配置示例{name:low-code/shared,version:1.0.0,main:dist/index.js,types:dist/index.d.ts,files:[dist],scripts:{build:tsc}}{name:low-code/backend,version:1.0.0,description:低代码平台后端服务 - 使用JSON文件存储,main:src/index.ts,scripts:{dev:ts-node-dev --respawn --transpile-only src/index.ts,build:tsc,start:node dist/index.js},dependencies:{express:^4.18.2,cors:^2.8.5,bcryptjs:^2.4.3,jsonwebtoken:^9.0.2,uuid:^9.0.0},devDependencies:{types/express:^4.17.21,types/cors:^2.8.17,types/bcryptjs:^2.4.6,types/jsonwebtoken:^9.0.5,types/uuid:^9.0.7,types/node:^20.10.0,typescript:^5.3.0,ts-node-dev:^2.0.0}}4.2 内部引用 vs 发布后引用开发时子包之间通过 workspace 协议引用{dependencies:{low-code/shared:workspace:*}}workspace:*指向本地包。发布时工具会自动将其替换为真实版本号如^1.0.0从 npm registry 安装。4.3 推荐的发布工具Changesets自动检测变更、管理版本号、批量发布Lerna经典的 Monorepo 发布工具Turborepo侧重构建缓存和任务调度也支持发布流程五、从零创建本项目 Monorepo 的完整步骤第一步初始化根目录mkdirlow-code-platformcdlow-code-platformgitinitnpminit-y编辑根目录package.json{name:low-code-platform,version:1.0.0,description:低代码平台 - 使用JSON文件存储,private:true,workspaces:[packages/shared,packages/backend,packages/frontend],scripts:{dev:backend:cd packages/backend npm run dev,dev:frontend:cd packages/frontend npm run dev,build:shared:cd packages/shared npm run build}}04关键点private: true是必须的防止根目录被意外发布到 npmworkspaces数组声明哪些目录是子包第二步创建子包目录结构mkdir-ppackages/shared/srcmkdir-ppackages/backend/srcmkdir-ppackages/frontend/src第三步初始化每个子包分别在每个子包目录下创建package.jsoncdpackages/sharednpminit-ycd../backendnpminit-ycd../frontendnpminit-ycd../..然后修改每个子包的name使用统一的 scope 前缀low-code/packages/shared/package.json{name:low-code/shared,version:1.0.0,description:低代码平台共享类型和工具,main:src/index.ts}packages/backend/package.json{name:low-code/backend,version:1.0.0,description:低代码平台后端服务 - 使用JSON文件存储,main:src/index.ts}packages/frontend/package.json{name:low-code/frontend,version:1.0.0,description:低代码平台前端应用,type:module}第四步安装各子包的依赖在根目录执行使用-w参数指定目标子包# shared 包npminstall-Dtypescript-wpackages/shared# backend 包npminstallexpress cors bcryptjs jsonwebtoken uuid-wpackages/backendnpminstall-Dtypescript ts-node-dev types/express types/cors types/bcryptjs types/jsonwebtoken types/uuid types/node-wpackages/backend# frontend 包npminstallreact react-dom react-router-dom axios-wpackages/frontendnpminstall-Dtypescript vite vitejs/plugin-react types/react types/react-dom-wpackages/frontendnpm workspaces 会自动将依赖提升hoist到根目录的node_modules子包之间共享相同版本减少重复安装。第五步建立子包之间的引用关系让backend和frontend引用shared包npminstalllow-code/shared-wpackages/backendnpminstalllow-code/shared-wpackages/frontendnpm 会自动识别这是 workspace 内部的包创建符号链接symlink而不是从 registry 下载。第六步配置 TypeScript每个子包创建各自的tsconfig.json。第七步添加根目录统一脚本{scripts:{dev:backend:cd packages/backend npm run dev,dev:frontend:cd packages/frontend npm run dev,build:shared:cd packages/shared npm run build}}第八步安装所有依赖npminstall在根目录执行一次npm 会扫描所有 workspace统一安装依赖处理好内部包的链接关系。六、常见的 Monorepo 工具对比工具特点适用场景npm workspacesNode.js 内置零配置中小型项目简单需求Yarn workspaces成熟稳定社区广泛中大型项目pnpm workspaces磁盘空间节省安装速度快对性能有要求的项目Turborepo智能构建缓存任务调度构建频繁的大型项目Nx功能全面依赖图分析企业级大型项目Lerna经典工具版本管理和发布需要频繁发包的项目本项目使用的是npm workspaces是最轻量的方案适合当前的项目规模。七 monorepo VS 微前端 VS npm发包1.Monorepo 多个项目放在一个仓库里通过 workspaces 共享代码。优点代码共享零成本改了公共包立刻生效不需要发版、不需要更新版本号统一的依赖管理避免不同项目用不同版本的同一个库导致的冲突原子提交一个 PR 可以同时改前端、后端、公共包保证一致性重构方便IDE 可以跨包跳转和重命名缺点仓库会越来越大clone 和 CI 构建变慢权限管理粗粒度所有人都能看到所有代码虽然可以用 CODEOWNERS 缓解构建工具配置复杂需要处理包之间的依赖关系和构建顺序所有项目耦合在一起一个包的 CI 挂了可能阻塞其他包的发布适合场景同一个团队维护的紧密关联的项目2.npm 发包后引入 公共代码发布到 npm或私有 registry其他项目通过 npm install 安装使用。优点版本管理清晰每个消费方可以锁定自己需要的版本升级节奏自主可控项目完全解耦各自独立仓库、独立 CI/CD适合跨团队共享不需要在同一个仓库里仓库体积小职责单一缺点开发调试麻烦改了公共包要先发版消费方再更新版本号才能用到新代码虽然可以用 npm link 缓解但体验一般版本碎片化不同项目可能用着不同版本的公共包容易出现兼容性问题发布流程有额外成本需要维护 changelog、语义化版本号等跨包重构困难改了公共包的接口不知道哪些消费方会受影响适合场景通用工具库、组件库需要被多个不相关的项目使用比如公司内部的 UI 组件库。3.微前端qiankun、Module Federation、single-spa 等多个独立前端应用在运行时组合成一个整体每个子应用可以独立开发、独立部署。优点技术栈无关子应用 A 用 React子应用 B 用 Vue完全没问题独立部署改了一个子应用只需要部署那一个不影响其他团队自治不同团队负责不同子应用互不干扰渐进式迁移老项目可以一块一块地迁到新技术栈缺点运行时开销需要加载框架的运行时、处理沙箱隔离、样式隔离等复杂度高子应用之间的通信、状态共享、路由协调都需要额外处理用户体验可能受影响子应用切换时可能有加载延迟公共依赖重复加载虽然可以通过 externals 或 shared 优化但配置复杂调试困难问题可能出在主应用、子应用、或者它们之间的交互适合场景大型企业级应用多个团队协作或者需要整合不同技术栈的遗留系统。QAQ1.针对monorepo得仓库而言其根目录是不是一定要有package.json文件A1.取决于你用的包管理工具。如果是npm/yarn workspaces则必须有。为 workspaces 的声明就写在根目录的 package.json 里没有它包管理器就不知道哪些子目录是 workspace // 根目录 package.json { workspaces: [ packages/shared, packages/backend, packages/frontend ] } 对于其他得例如pnpm则不一定但是需要 pnpm-workspace.yaml。Q2.项目 a 没有部署项目 b 和 c 还能用 a 的东西吗A2.这里要区分两个概念部署和安装依赖 部署是指把项目发布到线上运行这跟项目之间的代码引用没关系。 项目 b 和 c 引用项目 a 的东西发生在开发阶段和构建阶段。在构建build的时候a 的代码会被打包进 b 和 c 的产物里。所以最终 b 和 c 各自的部署包已经包含了 a 的代码a 本身不需要单独部署。 以当前得monorepo项目为例low-code/shared 这个包不会单独部署它的类型定义在构建时就被前端和后端各自打包进去了。Q3.是不是只要多个项目在同一个文件夹下就能互相引用不是光放在一起不够。需要做额外的配置才能让它们互相识别。常见的方式有npm/yarn/pnpm workspaces — 在根目录的 package.json 里声明 workspaces包管理器会自动把本地包链接起来。 // 根目录 package.json { workspaces: [packages/*] } 然后在 b 和 c 的 package.json 里声明依赖 { dependencies: { low-code/shared: * } } 这样包管理器在 npm install 时会把本地的 low-code/shared 通过 symlink符号链接链接到 b 和 c 的 node_modules 里而不是去 npm 仓库下载。、总结Monorepo 不是银弹但对于像本项目这样前后端 共享库的多包结构它提供了开发效率代码共享零成本修改即时生效一致性保障依赖版本统一接口变更原子提交灵活的发布策略开发在一起发布可以独立简化的工程配置一套 CI/CD一次依赖安装核心思路就是用一个仓库的便利性管理多个包的独立性。