当前位置: 首页 > news >正文

工程化(二):为什么你的下一个项目应该使用Monorepo?(pnpm / Lerna实战)

工程化(二):为什么你的下一个项目应该使用Monorepo?(pnpm / Lerna实战)

引子:前端项目的“孤岛困境”

随着你的项目或团队不断成长,一个棘手的问题会逐渐浮现:代码该如何组织?

最传统、最直观的方式,是**多仓库(Polyrepo)**模式:一个项目,一个Git仓库。

  • 你有一个my-awesome-app的前端应用仓库。
  • 你有一个my-shared-utils的共享工具函数仓库。
  • 你有一个my-ui-components的通用UI组件库仓库。

一开始,这看起来很美。每个项目职责单一,独立演进。但很快,你会陷入“孤岛困境”带来的痛苦之中:

  1. 依赖管理地狱

    • my-awesome-app依赖my-shared-utils1.0.0版本。
    • 现在,你为了修复一个bug,在my-shared-utils里发布了1.0.1版本。
    • 你必须回到my-awesome-app仓库,更新package.json,运行npm install,提交、发布,才能用上这个修复。
    • 如果my-ui-components也依赖了my-shared-utils呢?你需要把这个更新流程在每一个依赖它的仓库里都重复一遍!这个过程极其繁琐、耗时且容易出错。
  2. 原子性变更的缺失

    • 假设一个重大的功能变更,需要同时修改后端API、前端应用和共享组件库。这需要你在三个不同的仓库里,创建三个独立的Pull Request。
    • 这三个PR很难保证被同时合并。如果其中一个合并了,而另外两个没有,你的线上环境就可能处于一个不一致的、破碎的状态。
  3. 代码复用与重构的巨大阻力

    • 当你想把my-awesome-app中的一个通用函数,抽离到my-shared-utils中时,这个看似简单的操作,需要跨越两个仓库,流程瞬间变得复杂。
    • 大规模的重构(比如升级一个核心库的主版本)更是天方夜谭,因为它需要在所有相关的“孤岛”上同步进行。
  4. 开发环境的不一致

    • 每个仓库都有自己的一套eslint配置、typescript配置、构建脚本。保持它们之间的同步和一致,本身就是一项巨大的维护成本。

如果你正在经历这些痛苦,那么,是时候了解一种更现代、更高效的代码组织范式了——Monorepo


第一幕:Monorepo - 从“孤岛联邦”到“统一帝国”

Monorepo,即单体仓库(Monolithic Repository),其核心思想非常简单:

将多个逻辑上独立、但实际上互相依赖的项目,统一存储在同一个Git仓库中。

Google, Meta, Microsoft等许多大型科技公司,都在内部大规模地使用Monorepo来管理他们庞大而复杂的代码库。开源社区中,Babel, React, Vue, NestJS等知名项目,也无一例外地采用了Monorepo的组织方式。

一个典型的Monorepo文件结构可能长这样:

/my-monorepo
├── packages/
│   ├── app-a/
│   │   └── package.json
│   ├── app-b/
│   │   └── package.json
│   ├── shared-utils/
│   │   └── package.json
│   └── ui-components/
│       └── package.json
├── package.json        // 根package.json
├── pnpm-workspace.yaml // Monorepo配置文件
└── tsconfig.json       // 统一的TS配置

在这个结构中,packages目录下的每一个子目录,都是一个独立的、拥有自己package.json本地包(Local Package)

这看起来只是把多个项目文件夹放在了一起,但它在现代包管理工具(如pnpm, yarn, npm)的“workspace”特性的加持下,能爆发出惊人的威力,完美地解决了Polyrepo的四大痛点。


第二幕:pnpm Workspace - Monorepo的“魔力引擎”

虽然Lerna是Monorepo领域的老牌工具,但随着npm, yarn, pnpm等包管理器原生支持了workspace(工作区)功能,现代Monorepo的最佳实践,已经转向了**“包管理器 + 专用工具”**的组合。

其中,pnpm因其高效的磁盘空间利用和卓越的性能,成为了搭建Monorepo的首选。

pnpm workspace的核心魔力在于:它能自动地在本地包之间建立符号链接(Symbolic Link)

让我们回到那个依赖管理的噩梦。在Monorepo中,如果app-a依赖shared-utils,它的package.json会这样写:

// packages/app-a/package.json
{"name": "app-a","dependencies": {"shared-utils": "workspace:*" }
}

workspace:*这个特殊的版本号,告诉pnpm:“请在当前工作区内寻找一个名为shared-utils的包,并直接链接到它。”

当你运行pnpm install时,pnpm会在app-a/node_modules目录下,创建一个指向packages/shared-utils真实源文件的符号链接

这意味着

  • 无需发布,即时更新:当你在shared-utils里修改了代码,app-a立即感知到这个变化,无需任何版本发布和重装依赖的流程。本地开发调试的体验发生了质的飞跃。
  • 单一依赖版本:所有本地包都共享同一个根目录的node_modules。pnpm会通过其巧妙的算法,确保整个Monorepo中,同一个第三方依赖(比如React)只有一个版本被安装,从根本上杜绝了版本冲突和“依赖地狱”。

实战:改造我们的“看不见”应用

现在,我们就来把我们之前构建的、分散在不同章节的纯逻辑模块,改造成一个Monorepo。

步骤一:初始化项目结构

mkdir my-invisible-app-monorepo
cd my-invisible-app-monorepo
pnpm init

创建pnpm-workspace.yaml文件,这是声明一个pnpm工作区的标志:

# pnpm-workspace.yaml
packages:- 'packages/*'

这告诉pnpm,所有在packages/目录下的子目录,都将被视为工作区内的本地包。

创建packages目录,并把我们之前的核心逻辑,拆分成独立的包:

/my-invisible-app-monorepo
├── packages/
│   ├── rendering-engine/  (存放vdom, diff, patch等)
│   │   └── package.json
│   ├── state-management/  (存放atom, store等)
│   │   └── package.json
│   └── app-core/          (作为主应用,消费其他包)
│       └── package.json
└── pnpm-workspace.yaml
└── package.json

步骤二:配置各个包的package.json

packages/rendering-engine/package.json

{"name": "@invisible/rendering-engine","version": "1.0.0","main": "dist/index.js", // 假设我们有构建步骤"types": "dist/index.d.ts"
}

packages/state-management/package.json

{"name": "@invisible/state-management","version": "1.0.0","main": "dist/index.js","types": "dist/index.d.ts"
}

packages/app-core/package.json

{"name": "@invisible/app-core","version": "1.0.0","dependencies": {"@invisible/rendering-engine": "workspace:*","@invisible/state-management": "workspace:*"},"scripts": {"start": "node ./src/main.js"}
}

步骤三:安装依赖

回到项目根目录,运行:

pnpm install

pnpm会自动读取所有packages/*/package.json,安装它们的依赖,并在app-corenode_modules下创建指向rendering-enginestate-management的符号链接。

步骤四:在app-core中使用本地包

现在,app-core可以像消费NPM上的普通包一样,消费我们自己的本地包。

packages/app-core/src/main.js

// 像引用第三方库一样,引用我们自己的本地包
const { createElement, diff } = require('@invisible/rendering-engine');
const { atom, AtomStore } = require('@invisible/state-management');console.log('Successfully imported local packages from workspace!');// ... 你的应用主逻辑 ...

步骤五:统一的脚本命令

我们可以在根目录的package.json中,使用-r--recursive标志来执行所有子包的脚本,或者用--filter来指定某个包。

package.json

{"scripts": {"build": "pnpm --recursive build", // 运行所有包的build脚本"start:app": "pnpm --filter @invisible/app-core start" // 只运行app-core的start脚本}
}

现在,在根目录运行pnpm start:app,就可以启动我们的主应用了。


第三幕:Monorepo工具链 - Lerna与Changesets

虽然pnpm workspace解决了本地依赖和脚本执行的问题,但对于更复杂的Monorepo管理,比如版本控制发布流程,我们还需要更专业的工具。

Lerna:老牌的版本与发布管理者

Lerna是一个Monorepo管理工具,它最核心的功能是:

  • 版本管理lerna version可以智能地检测自上次发布以来,哪些包发生了变更,并根据你的配置(固定模式或独立模式),自动提升它们的版本号、打上git tag。
  • 发布流程lerna publish会将所有版本有变更的包,一键发布到NPM。

现代工作流中,Lerna通常与pnpm workspace结合使用,pnpm负责依赖管理,Lerna负责版本和发布。

Changesets:更现代化的选择

Changesets是Atlassian推出的一个更现代化的Monorepo版本管理工具。它采用了一种更优雅的、基于“意图”的工作流:

  1. 当你完成一个功能或修复(可能跨越了多个包)后,你运行pnpm changeset add
  2. 工具会交互式地询问你,哪些包受到了影响,以及这次变更是patch(修复)、minor(功能)还是major(破坏性变更)。
  3. 它会生成一个.md文件,记录下这次变更的“意图”。
  4. 在发布时,pnpm changeset version会读取所有这些.md文件,自动计算出每个包的下一个正确版本,并生成更新日志(CHANGELOG)。
  5. 最后,pnpm publish -r(或lerna publish)将它们发布。

这种工作流将版本决策,分散到了每一次的开发提交中,让发布过程变得更加自动化和可预测。

结论:Monorepo是团队协作的“加速器”

从Polyrepo到Monorepo,不仅仅是代码文件夹的“物理聚合”,更是研发流程和团队协作模式的一次深刻变革。

通过将所有相关的代码置于一个统一的仓库和工具链下,Monorepo为我们带来了:

  • 无摩擦的本地开发workspace:*协议消除了本地包之间调试和联动的延迟。
  • 强化的代码一致性:统一的构建、测试、Lint和类型检查,保证了整个代码库的高质量。
  • 简化的依赖管理:从根本上解决了版本冲突和依赖更新的繁琐工作。
  • 高效的跨项目重构:IDE的重构功能(如重命名、文件移动)可以在整个代码库中原子化地完成。
  • 透明的代码共享文化:所有代码都在眼前,鼓励了团队成员之间的代码复用和互相学习。

当然,Monorepo也并非银弹。它对构建工具链的要求更高,仓库的体积和历史可能会变得非常庞大。但对于任何需要多个包协同工作、或者期望促进团队内部代码共享的项目来说,它带来的收益,远远超过了它的成本。

核心要点:

  1. **多仓库(Polyrepo)**模式在依赖管理、原子性提交和代码重构方面存在显著痛点。
  2. **单体仓库(Monorepo)**通过将多个项目放在一个仓库中,来解决这些问题。
  3. **pnpm workspace**等工具是Monorepo的引擎,它通过符号链接实现了本地包之间的即时联动。
  4. LernaChangesets等专业工具,则进一步解决了Monorepo的版本管理和发布流程的自动化问题。
  5. Monorepo是一种促进团队协作、提升工程效率的先进代码组织范式。

至此,我们第四部分《性能与工程化》的探索也告一段落了。我们的“看不见”的应用,不仅性能卓越,而且拥有了现代化的、可扩展的工程化架构。

在最后的第五部分**《思想升华与未来》**中,我们将从具体的代码实现,上升到更宏观的设计模式、自动化流程和工程师的职业哲学。敬请期待!

http://www.lryc.cn/news/607335.html

相关文章:

  • R 语言文件读写、批量读取与图片保存实用代码汇总
  • 逻辑回归参数调优实战指南
  • 【Linux系列】Vim 中删除当前单词
  • Master Prompt:AI时代的万能协作引擎
  • 法国彩虹重磅发布EmVue:解锁能源监控新方式
  • 使用 Trea cn 设计 爬虫程序 so esay
  • 【Jetson orin-nx】使用Tensorrt并发推理四个Yolo模型 (python版)
  • Git 各场景使用方法总结
  • JVM、JDK、JRE的区别
  • 如何快速给PDF加书签--保姆级教程
  • vue2实现类似chatgpt和deepseek的AI对话流打字机效果,实现多模型同时对话
  • 在PyCharm中将现有Gitee项目重新上传为全新项目
  • 单变量单步时序预测:CNN-LSTM卷积神经网络结合长短期记忆神经网络
  • 服务器问题调试-线上系统退出时的一般解决思路
  • 以太网是什么网,什么网是以太网
  • 隧道安全监测哪种方式好?精选方案与自动化监测来对比!
  • 从 0 到 1 认识 Spring MVC:核心思想与基本用法(下)
  • JP3-3-MyClub后台后端(二)
  • 携程PMO资深经理、携程技术委员会人工智能委员会秘书陈强受邀为PMO大会主持人
  • 如何在Android中创建自定义键盘布局
  • S7-1200 /1500 PLC 进阶技巧:组织块(OB1、OB10)理论到实战
  • 高速信号设计之 DDR5 篇
  • 吃透 B + 树:MySQL 索引的底层逻辑与避坑指南
  • 大模型应用
  • 译 | BBC Studios团队:贝叶斯合成控制方法SCM的应用案例
  • Ant Design Vue notification自定义
  • iOS企业签名掉签,iOS企业签名掉签了怎么办?
  • H5 列表页返回后保持数据的解决方案总结(以 Vue 3 为例)
  • 【网安播报】Lazarus Group 利用开源包展开长期供应链间谍战
  • AUTOSAR进阶图解==>AUTOSAR_SRS_E2E