工程化(二):為什么你的下一個項目應該使用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.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;