工程化(二):为什么你的下一个项目应该使用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.pswp.cn/bicheng/91606.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91606.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91606.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

应用药品注册证识别技术,为医药行业的合规、高效与创新发展提供核心驱动力

在医药行业的庞杂数据海洋中,药品注册证(如中国的“国药准字”、美国的NDA/ANDA批号)是药品合法上市流通的“身份证”。面对海量的证书审核、录入与验证需求,传统人工处理方式不仅效率低下、成本高昂,更易因疲劳导致差…

Spring Boot 2.1.18 集成 Elasticsearch 6.6.2 实战指南

Spring Boot 2.1.18 集成 Elasticsearch 6.6.2 实战指南前言:一. JAVA客户端对比二. 导入数据2.1 分析创建索引2.2 代码实现三. ElasticSearch 查询3.1 matchAll 查询3.2 term查询3.3 match查询3.4 模糊查询3.5 范围查询3.6 字符串查询3.7 布尔查询3.8 分页与排序3.…

向量投影计算,举例说明

向量投影计算,举例说明 向量投影是指将一个向量(设为向量b\mathbf{b}b)投射到另一个向量(设为向量a\mathbf{a}a)所在直线上,得到一个与a\mathbf{a}

如何在技术世界中保持清醒和高效

“抽象泄露,是存在的,但你需要了解多少,需要理解多深,这一点是因人而异的,绝对不是别人能够建议的。每个人只会站在自己的立场上去建议别人怎么做。”在写下这句话时,身为一个技术开发者,我似乎…

服装公司数字化转型如何做?

WL贸易集团公司(以下简称WL)自2012年成立以来,在十余年的发展历程中不断蜕变与升级。公司始终秉持“时尚与品质优先”的核心经营理念,通过严格执行高标准、严要求,牢牢把握产品品质与交货周期两大关键,赢得…

GM DC Monitor 之 银河麒麟 Docker 部署安装手册

官方网站&#xff1a;www.gm-monitor.com 本手册以银河麒麟为例&#xff0c;介绍在 Linux 系统上安装和配置DOCKER服务的详细步骤 一、以root用户执行以下操作命令 1、环境优化 modprobe br_netfilter cat <<EOF > /etc/sysctl.d/docker.conf net.bridge.bridge-n…

网络编程接口bind学习

1、概述下面2个问题你会怎么回答呢?1、bind如果绑定0号端口&#xff0c;可以工作么&#xff0c;如果能正常工作&#xff0c;绑定的什么端口 2、客户端可以调用bind么2、解析2.1、bind如果绑定0号端口&#xff0c;可以工作么&#xff0c;如果能正常工作&#xff0c;绑定的什么端…

FinOps X 2025 核心发布:AI 时代下的 FinOps 转型

2025年&#xff0c;人工智能技术的突破性发展正深刻重塑商业与技术格局&#xff0c;智能技术已成为各领域创新的核心驱动力。在此背景下&#xff0c;FinOps X 2025 围绕 AI 技术对财务运营&#xff08;FinOps&#xff09;的革新作用展开深度探讨&#xff0c;重点呈现了以下关键…

使用Min-Max进行数据特征标准化

在数据处理过程中&#xff0c;标准化是非常重要的步骤之一&#xff0c;特别是在机器学习和数据分析中。Min-Max标准化&#xff08;也称为归一化&#xff09;是一种常用的数据标准化方法&#xff0c;它通过将数据缩放到一个指定的范围&#xff08;通常是0到1之间&#xff09;&am…

【Dart 教程系列第 51 篇】Iterable 中 reduce 函数的用法

这是【Dart 教程系列第 51 篇】,如果觉得有用的话,欢迎关注专栏。 博文当前所用 Dart SDK:3.5.4 文章目录 一:reduce 作用 二:举例说明 1:求和 2:查找最大/最小值 3:字符串拼接 4:自定义对象合并 三:注意事项 一:reduce 作用 reduce 是 Iterable 的一个方法,用于…

使用VSCode配置Flutter

本周&#xff08;学期第四周&#xff09;任务&#xff1a; 1.简单学习Flutter&#xff0c;完成环境安装与配置 2.探索Flutter与Unity集成方案 一、Flutter环境配置 根据Flutter官方文档进行环境配置&#xff1a;开发 Android 应用 | Flutter 中文文档 - Flutter 中文开发者网…

React 开发中遇见的低级错误

1.useState不起效果 异步 改用 useRef2.map循环{ WechatQuestionnaireData && WechatQuestionnaireData?.questions?.map((item: any) > (<div className{styles[title]}>{item.questionTitle}</div>))}注意这里的 》 后面是括号 我开始写成{} 好久…

iphone手机使用charles代理,chls.pro/ssl 后回车 提示浏览器打不开该网页

iphone手机使用charles代理,chls.pro/ssl 后回车 提示浏览器打不开该网页) 1、问题现状&#xff1a; Charles安装证书异常问题&#xff0c;网页访问chls.pro/ssl提示网页打不开&#xff0c;在charles页面有链接&#xff0c;可以看到http请求和https就是看不到详细内容 2、解决方…

第11届蓝桥杯Python青少组_国赛_高级组_2020年10月真题

第11届蓝桥杯Python青少组_国赛_高级组_2020年10月真题 更多内容请查看网站&#xff1a;【试卷中心 -----> 蓝桥杯----> Python ----> 国赛】 网站链接 青少年软件编程历年真题模拟题实时更新 一、选择题 第 1 题 执行以下程序,输出的结果是 ( )。 print( 0.1 …

如何处理Y2K38问题

一、什么是Y2K38问题Y2K38 问题&#xff0c;也称为 2038年问题&#xff0c;是一个类似于Y2K问题的计算机日期处理问题。1、什么是Y2K38 问题&#xff1f;Y2K38 问题是指在计算机系统中&#xff0c;某些使用 32位有符号整数 来存储时间的程序&#xff0c;将在 2038年1月19日03时…

LeetCode热题100——146. LRU 缓存

https://leetcode.cn/problems/lru-cache/description/?envTypestudy-plan-v2&envIdtop-100-liked 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓…

一个Pycharm窗口添加多个项目来满足运行多个项目的需求

需求&#xff1a;此前项目文件只有D:\pythonProject 现在进行了如下操作 同时显示两个文件夹D:\pythonProject D:\pythonProject-gh操作步骤如下&#xff1a;最终结果如图所示

mars3d实现省界线宽度>市界线宽度效果

效果图&#xff1a; 实现代码&#xff1a; export function showChinaLine() {map.basemap 2017graphicLayer new mars3d.layer.GeoJsonLayer({name: "全国省界",url: "https://data.mars3d.cn/file/geojson/areas/420000_full.json",format: simplifyG…

Stack、Queue and Deque

文章目录一、适配器二、stcak模拟实现三、queue模拟实现四、vector和list的优缺点五、deque六、deque的优缺点七、deque为什么作为stack和queue的默认适配容器一、适配器1.适配器的概念&#xff1a;封装一个已有对象&#xff0c;转换其接口2.容器适配器&#xff1a;封装一个已有…

[echart] Vue3中使用Echart时图表不渲染

onMounted(() > {nextTick(() > {chartInstance echarts.init(document.getElementById(chart));chartInstance.setOption(option);}); });参考&#xff1a; Vue3中使用Echart时如何解决图表不渲染或显示空白的问题&#xff1f;