惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
腾讯CDC
G
Google Developers Blog
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
美团技术团队
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志

Grtsinry43 的前端札记

一些忙碌之外的余温 我是怎样使用 AI Agent 写代码的 从头开始的勇气 列车上的生活 分享一点体测的小事 「手搓系列 02」深入浅出 Agent Skills:架构、实现与思考 路的尽头还是路 二十一岁,然后...... 我患上了 token 的瘾 Xiaomi 17 标准版刷机折腾记:解锁、官改ROM与必备模块 思考 在焦虑与代码中缓慢前行 从 v1 到 v2,谈谈这个简单博客背后的架构演进与实现 Go 语言初体验:Less is more,一种丑但可靠的工程美学 职规赛,一次特别的体验 2025 年终总结——从晨光到雾散,化经历为成长 2025 实习札记:关于一场未完成的迁徙与落地 新时代的 PHP:RSC 的边界错位与工程代价 从想法到实践:在无序的生活里,试图用代码敲出一点秩序 PureFlow 简析:用 Kotlin 跨平台构建 RSS 阅读器 命运开了个无情的玩笑 「手搓系列 01」 从零搭建 Vue 文档站,学习从静态生成到语法解析 好久不见,这是一份来自作者的独白
手把手带你玩转 Monorepo,拥抱现代前端开发新范式
grtinry43 · 2025-07-21 · via Grtsinry43 的前端札记

你是否曾被这些问题困扰?

  • 管理多个相互关联的 Git 仓库(或是复杂的 git submodules),心力交瘁。
  • 想在项目 A 中复用项目 B 的组件或工具函数,只能复制粘贴或者发布成 npm 包,流程繁琐。
  • 多个项目依赖同一个库,版本不一致导致“依赖地狱”。
  • 每次进行一个跨项目的需求,需要在多个仓库中提交代码、创建 PR,联调测试苦不堪言。

如果你对以上场景感同身受,无论是组件库,框架,或是日常的大型项目,不妨试试 Monorepo。

到底什么是 Monorepo?

想必大家听过这个概念无数次了

Monorepo(Monolithic Repository),直译为“单体仓库”,是一种将多个独立的项目、包(package)或应用(app)存放在同一个代码仓库中进行管理的代码组织策略。

与之相对的是 Polyrepo(Multiple Repositories),也就是我们传统的多仓库管理模式,每个项目都有自己独立的 Git 仓库。

对比项Monorepo(单体仓库)Polyrepo(多仓库)
代码组织所有项目在一个仓库中每个项目一个独立仓库
依赖管理根目录统一管理,易于保持版本一致各项目独立管理,易产生版本冲突
代码复用极为方便,通过工作区(workspace)直接引用需发布 npm 包或 Git Submodule,流程复杂
原子提交一次提交可跨越多个项目,保证原子性无法实现跨项目的原子提交
构建与部署工具链复杂,但可实现统一构建和按需部署各项目独立,简单直接
协作团队成员可见所有代码,便于协作和代码审查边界清晰,便于权限管理

一个常见的误解:Monorepo ≠ 单体应用(Monolith)

  • Monorepo 是一种 代码组织方式。仓库里可以包含多个独立的、可独立部署的应用。
  • 单体应用 是一种 软件架构模式。它指的是将所有功能模块打包成一个单一的、不可分割的部署单元。

例如在 Monorepo 中,我们可以同时管理一个 React 主应用、一个 Vue 管理后台、一个共享的 UI 组件库和一个通用的工具函数库。它们虽然在同一个仓库,但架构上是解耦的,可以独立开发、测试和部署。

为什么选择 Monorepo?

优势

  1. 极致的代码复用与共享:这是 Monorepo 最核心的优势。UI 组件库、工具函数、TS 类型定义等可以作为本地包,被仓库内的任何应用直接引用,无需发布到 npm。修改后立即生效,开发体验如丝般顺滑。
  2. 简化的依赖管理:所有项目共享同一个 node_modules(或其变体),借助 pnpm 等工具可以有效解决依赖版本冲突问题,保证环境一致性。
  3. 原子化的提交(Atomic Commits):当一个功能需要同时修改前端应用和其依赖的组件库时,可以在一次提交中完成所有更改。这让代码历史追溯和回滚变得异常清晰。
  4. 统一的工具链与标准化:可以在仓库根目录配置一次 ESLint, Prettier, TypeScript, Jest 等,所有子项目共同遵守,确保了代码风格和质量的统一。
  5. 提升团队协作:代码透明度高,便于团队成员进行跨项目的 Code Review 和知识共享。

挑战

  1. 工具链复杂度:需要引入 Lerna, Nx, Turborepo 等专门的工具来管理工作区、任务调度和构建缓存,有一定的学习成本。
  2. 性能问题:当仓库变得非常巨大时,git clone, git status 等命令可能会变慢。不过现代工具正在努力解决这个问题。
  3. 权限控制:默认情况下,所有人都拥有所有代码的访问权限。对于需要精细化权限控制的团队,需要借助如 GitLab/GitHub CODEOWNERS 等功能。

总的来说,对于需要高度协作、代码共享频繁的前端团队,Monorepo 带来的收益远大于其挑战。

选择合适的 Monorepo 工具

工欲善其事,必先利其器。现代 Monorepo 生态已经非常成熟,以下是几个主流工具:

  • 包管理器(必须)
  • pnpm: 目前 Monorepo 的首选。它通过符号链接(symlinks)和内容寻址存储来高效管理 node_modules,天生支持 workspace(工作区)协议,完美契合 Monorepo 场景。
  • npm (v7+) / yarn (v2+):也都支持 workspace,但 pnpm 在性能和磁盘空间占用上更具优势。
  • 任务编排与构建系统(强烈推荐)
  • Turborepo: 由 Vercel(Next.js 的母公司)出品,主打“高性能构建系统”。它通过智能任务调度和远程缓存,可以极大地提升 CI/CD 和本地开发的速度。简单、快速、易于上手,是目前的热门选择。
  • Nx: 功能极其强大且全面的 Monorepo 工具集。除了 Turborepo 的功能外,还提供了代码生成、依赖图可视化、插件生态等企业级功能,但配置也相对复杂。

这篇文章将教你从基础 pnpm workspaces,到引入 truborepo 加速构建,再使用自建缓存代替 Vercel Remote Cache。

熟悉使用

下面是 pnpm 在 Monorepo 中常用的基本操作:

初始化 Monorepo

首先,你需要一个项目根目录。在根目录下创建 pnpm-workspace.yaml 文件,这是 pnpm 识别 Monorepo 的关键。

pnpm-workspace.yaml 文件定义了你的工作区(workspace)包含哪些子包。

pnpm-workspace.yaml 示例:

创建子包(Packages)

pnpm-workspace.yaml 中定义的路径下创建你的子包。例如,如果你设置了 packages/*,那么可以在 packages 目录下创建 package-apackage-b

安装依赖

在 Monorepo 根目录运行 pnpm install 会安装所有子包的依赖,并且 pnpm 会自动识别并符号链接(symlink)工作区内的互相依赖。

添加/移除依赖

添加通用依赖(安装到所有子包)

如果你想在所有子包中添加相同的依赖,可以使用 -w--workspace-root 参数在根目录操作,但通常这不常用。更常见的是给特定子包添加依赖。

添加特定子包依赖

进入子包目录,像普通项目一样添加依赖。pnpm 会智能地处理依赖关系。

添加工作区内部依赖

当一个子包需要依赖 Monorepo 内的另一个子包时,可以直接使用子包的名称(即 package.json 中的 name 字段)作为依赖。

假设 package-aname@my-monorepo/package-apackage-bname@my-monorepo/package-b

提示: 使用 workspace:*workspace:^ 可以更好地管理内部依赖的版本。pnpm 默认会使用 workspace:^

移除依赖

与添加依赖类似,进入子包目录或在根目录使用 -w

运行脚本

在 Monorepo 中,你可以从根目录运行特定子包的脚本,也可以运行所有子包的通用脚本。

运行特定子包的脚本

使用 -F--filter 参数指定要运行脚本的子包。

运行所有子包的脚本

pnpm -rpnpm recursive 命令可以在所有工作区包中运行指定的脚本。

发布子包

发布子包时,你需要进入相应的子包目录进行操作。

7. 一些有用的 pnpm 命令

  • pnpm ls -r:列出所有工作区包及其依赖。
  • pnpm outdated -r:检查所有工作区包的过时依赖。
  • pnpm up -r:更新所有工作区包的依赖。
  • pnpm store prune:清理本地 pnpm 存储,删除未引用的包。

实战:从零搭建一个前端 Monorepo

好吧,光说不做没有任何作用,讲这些东西没啥意思,csdn 分分钟给我抄走,ai 几秒钟就能生成,咱们实践才能出真知,Let’s get our hands dirty

我们的目标是建一个 Vue3 组件库,包含 storybook 文档站 单测 cypress 端测。

核心技术栈选择:

  • Vue 3: 利用 Composition API 和更好的性能。
  • TypeScript: 为组件库提供类型安全和更好的开发体验。
  • Vite: 用于组件库的构建和 Storybook 的开发服务器,速度快。
  • pnpm / yarn / npm (with workspaces): 推荐 pnpm 或 yarn workspaces 来管理 monorepo。这里以 pnpm 为例,因为它对 monorepo 支持良好且高效。
  • Storybook: 用于组件的交互式开发、文档和展示。
  • Vitest: 用于单元/组件测试,与 Vite 集成良好。
  • Vue Test Utils: Vue 官方的组件测试库。
  • Cypress: 用于端到端 (E2E) 测试。
  • ESLint & Prettier: 代码规范和格式化。
  • Husky & lint-staged: Git 钩子,在提交前自动检查和格式化代码。
  • Turborepo: 一个优秀的高性能构建系统,用于 JavaScript/TypeScript monorepos。

环境准备与项目初始化

我们这里就用我自己刚刚开始写的项目 amore-ui 为例。

确保你已经安装了 Node.js (我建议还是最新 lts v22 吧)。然后全局安装 pnpm:

现在,创建我们的项目:

初始化项目和 Monorepo (使用 pnpm):

编辑 pnpm-workspace.yaml:

在根目录安装通用开发依赖:

创建主组件库

先从我们的组件库开始!

创建组件库包 (packages/components):

安装组件库特定依赖:

配置 packages/components/vite.config.ts (用于库构建):

配置 packages/components/package.json:

创建 packages/components/src/index.ts:

创建示例组件 packages/components/src/components/Button/Button.vue:

创建 Storybook 应用

Storybook 是组件库开发使用的利器!

作为应用,我们将其放置在 app/ 目录,仅需在你建好的文件夹中执行

之后修改 .storybook/main.js

创建组件的 Story (packages/components/src/components/Button/Button.stories.ts):

修改根 package.json 的 scripts:

[!info]

上文的注释提到,这里详细解释一下,-f,即 filter,意为过滤器,也就是在对应的仓库中执行,-f 之后跟随的仓库名就是你 package.json 中为每个模块配置的名字,利用这种功能,我们可以为主仓库添加很多 模块:命令 的快捷命令

现在可以运行 pnpm dev:storybook 来启动 Storybook。

设置 Vitest (单元/组件测试):

好的项目通常都有高的单元测试覆盖率

packages/components 包中安装 Vitest 和 Vue Test Utils:

配置 packages/components/vite.config.ts (添加 test 配置): (在现有 defineConfig 内添加 test 字段)

创建 packages/components/vitest.setup.ts (可选):

packages/components/package.json 添加测试脚本:

写一个测试 (packages/components/src/components/Button/Button.test.ts):

运行 pnpm -F amore-ui test

设置 Cypress (E2E 测试):

趁热打铁,让我们继续!接下来是端到端测试

配置 cypress/cypress.config.ts:

在根 package.json 添加 Cypress 脚本:

  • start-server-and-test 是一个有用的 npm 包,可以帮你启动服务器,等待它响应,然后运行测试,最后关闭服务器。pnpm add -Dw start-server-and-test
  • dev:storybook-static 脚本可以是你构建 Storybook 后用 http-server 或类似工具启动静态文件的命令,例如: pnpm build:storybook && http-server storybook-static -p 6006

创建 E2E 测试 (cypress/e2e/button.cy.ts):

确保 Storybook 在 http://localhost:6006 运行,然后执行 pnpm cy:open

Okok,到这里大家可能都看累了或者是感觉无聊,我们来小小整理下我们有了什么命令呢:

可以休息下,下面我们继续

创建文档站

这部分很简单,我们依然靠 Vite 来实现

就像往常一样建好你的文档站,之后…

我这里使用了 unplugin-vue-components,当然你也可以从工作区直接导入打包之后的产物

到此为止,这个 monorepo 已经初具形态了。

ESLint, Prettier, Husky, lint-staged:

好的代码建立在规范之上

我的项目一直都是 eslint error 模式+有 eslint/单测/e2e 测试不过就禁止 commit,也就是受虐模式

  • Husky & lint-staged:

.husky/pre-commit 内容:

在根 package.json 添加 lint-staged 配置:

TypeScript 配置 (tsconfig.json):

太多了,贴不过来了,影响正常阅读

具体看我的仓库 amore-ui

引入 Turborepo 提升效率

目前,我们需要手动进入每个目录去运行命令。当项目变多时,这会变得很麻烦。Turborepo 可以帮我们统一管理和加速这些任务。

1. 在根目录安装 Turborepo

2. 配置 turbo.json

在项目根目录创建 turbo.json 文件:

3. 在根 package.json 中添加脚本

现在,你可以从根目录统一运行命令了!

Turborepo 自建缓存

我对 Vercel 公司不喜欢也不讨厌,但是我希望自建一个缓存。

参考这个项目就好啦:

Turborepo Remote Cache https://adirishi.github.io/turborepo-remote-cache-cloudflare/introduction/getting-started

Monorepo 最佳实践

  1. 统一配置:将 ESLint, Prettier, tsconfig.json 等配置文件放在 packages 目录下(如 packages/eslint-config-custom, packages/tsconfig),然后让各个应用和包去继承这些配置,保持一致性。
  2. 明确的目录结构apps 放应用,packages 放可复用包,是一种广泛采纳的约定。
  3. 版本管理:使用如 Changesets 这样的工具来管理包的版本发布和生成 CHANGELOG,它与 Monorepo 配合得非常好。
  4. 精简根目录:保持根目录 package.jsondependencies 干净,只存放对整个项目都至关重要的开发依赖(如 turbo, typescript, prettier)。

尾声

恭喜你!你已经成功搭建并体验了一个现代化的前端 Monorepo 项目。

感觉这个例子选的不是很好啊,有点太上难度了,直接把我之前研究好久的架子全搬上来了,一般只有组件库或者大框架会这样操作了,不过基础的入门操作还是可以提升效率的🥹

从现在开始拥抱现代前端开发吧!