通過上一節(jié)的學(xué)習(xí)续挟,我們看到了前端構(gòu)建工具及其背后蘊(yùn)含的技術(shù)設(shè)計(jì)。前端工程化包羅萬象侥衬,本節(jié)課诗祸,我們將分析項(xiàng)目組織設(shè)計(jì)的相關(guān)話題,包括:
接下來轴总,我們通過 2 節(jié)內(nèi)容來學(xué)習(xí)這個(gè)主題直颅。希望結(jié)束這個(gè)主題后大家可以從更高的視角看待項(xiàng)目管理和代碼組織設(shè)計(jì)。
大型前端項(xiàng)目的組織設(shè)計(jì)
隨著業(yè)務(wù)復(fù)雜度的直線上升怀樟,前端項(xiàng)目不管是從代碼量上功偿,還是從依賴關(guān)系上都爆炸式增長。同時(shí)往堡,團(tuán)隊(duì)中一般不止有一個(gè)業(yè)務(wù)項(xiàng)目械荷,多個(gè)項(xiàng)目之間如何配合共耍,如何維護(hù)相互關(guān)系?公司自己的公共庫版本如何管理吨瞎?這些話題隨著業(yè)務(wù)擴(kuò)展痹兜,紛紛浮出水面。一名合格的高級前端工程師颤诀,在宏觀上必需能妥善處理這些問題字旭。
當(dāng)然,不是每個(gè)開發(fā)者都有機(jī)會接觸項(xiàng)目設(shè)計(jì)着绊。如果讀者沒有面對過上述難題谐算,也許并不容易理解這些問題究竟意味著什么。舉個(gè)例子归露,團(tuán)隊(duì)主業(yè)務(wù)項(xiàng)目名為:App-project,這個(gè)倉庫依賴了組件庫:Component-lib斤儿,因此 App-project 項(xiàng)目的 package.json 會有類似的代碼:
{
"name": "App-project",
"version": "1.0.0",
"description": "This is our main app project",
"main": "index.js",
"scripts": {
"test": "echo \\"Error: no test specified\\" && exit 1"
},
"dependencies": {
"Component-lib": "^1.0.0"
}
}
這時(shí)新的需求來了剧包,產(chǎn)品經(jīng)理需要更改 Component-lib 組件庫中的 modal 組件樣式及交互行為。作為開發(fā)者往果,我們需要切換到 Component-lib 項(xiàng)目疆液,進(jìn)行相關(guān)需求開發(fā),開發(fā)完畢后進(jìn)行測試陕贮。這里的測試包括 Component-lib 當(dāng)中的單元測試堕油,當(dāng)然也包括在實(shí)際項(xiàng)目中進(jìn)行效果驗(yàn)收。為方便調(diào)試肮之,有經(jīng)驗(yàn)的開發(fā)者也許會使用 npm link/yarn link 來開發(fā)和調(diào)試效果掉缺。當(dāng)確認(rèn)一切沒問題后,我們還需要 npm 發(fā)包 Component-lib 項(xiàng)目戈擒,并提升版本為 1.0.1眶明。在所有這些都順利完成的基礎(chǔ)上,才能在 App-project 項(xiàng)目中進(jìn)行升級:
{
//...
"dependencies": {
"Component-lib": "^1.0.1"
}
}
這個(gè)過程已經(jīng)比較復(fù)雜了筐高。如果中間環(huán)節(jié)出現(xiàn)任何紕漏搜囱,我們都要重復(fù)上述所有步驟。另外柑土,這只是單一依賴關(guān)系蜀肘,現(xiàn)實(shí)中 App-project 不可能只依賴 Component-lib。這種項(xiàng)目管理的方式無疑是低效且痛苦的稽屏。那么在項(xiàng)目設(shè)計(jì)哲學(xué)上扮宠,有更好的方式嗎?
monorepo 和 multirepo
答案是肯定的诫欠,管理組織代碼的方式主要分為兩種:
- multirepo
- monorepo
顧名思義涵卵,multirepo 就是將應(yīng)用按照模塊分別在不同的倉庫中進(jìn)行管理浴栽,即上述 App-project 和 Component-lib 項(xiàng)目的管理模式;而 monorepo 就是將應(yīng)用中所有的模塊一股腦全部放在同一個(gè)項(xiàng)目中轿偎,這樣自然就完全規(guī)避了前文描述的困擾典鸡,不需要單獨(dú)發(fā)包、測試坏晦,且所有代碼都在一個(gè)項(xiàng)目中管理萝玷,一同部署上線,在開發(fā)階段能夠更早地復(fù)現(xiàn) bug昆婿,暴露問題球碉。
這就是項(xiàng)目代碼在組織上的不同哲學(xué):一種倡導(dǎo)分而治之,一種倡導(dǎo)集中管理仓蛆。究竟是把雞蛋放在同一個(gè)籃子里睁冬,還是倡導(dǎo)多元化,這就要根據(jù)團(tuán)隊(duì)的風(fēng)格以及面臨的實(shí)際場景進(jìn)行選型看疙。
我試著從 multirepo 和 monorepo 兩種處理方式的弊端說起豆拨,希望給讀者更多的參考和建議。
multirepo 存在以下問題:
- 開發(fā)調(diào)試以及版本更新效率低下
- 團(tuán)隊(duì)技術(shù)選型分散能庆,不同的庫實(shí)現(xiàn)風(fēng)格可能存在較大差異(比如有的庫依賴 Vue施禾,有的依賴 React)
- changelog 梳理困難,issues 管理混亂(對于開源庫來說)
而 monorepo 缺點(diǎn)也非常明顯:
- 庫體積超大搁胆,目錄結(jié)構(gòu)復(fù)雜度上升
- 需要使用維護(hù) monorepo 的工具弥搞,這就意味著學(xué)習(xí)成本比較高
清楚了不同項(xiàng)目組織管理的缺點(diǎn),我們再來看一下社區(qū)上的經(jīng)典選型案例渠旁。
Babel 和 React 都是典型的 monorepo攀例,其 issues 和 pull requests 都集中到唯一的項(xiàng)目中,changelog 可以簡單地從一份 commits 列表梳理出來一死。我們參看 React 項(xiàng)目倉庫肛度,從目錄結(jié)構(gòu)即可看出其強(qiáng)烈的 monorepo 風(fēng)格:
react-16.2.0/
packages/
react/
react-art/
react-.../
因此,react 和 react-dom 在 npm 上是兩個(gè)不同的庫投慈,它們只不過在 React 項(xiàng)目中通過 monorepo 的方式進(jìn)行管理承耿。至于為什么 react 和 react-dom 是兩個(gè)包,我把這個(gè)問題留給讀者伪煤。
而著名的 Rollup 目前是 multirepo 方式加袋。對于 monorepo 和 multirepo,選擇了 monorepo 的 Babel 貢獻(xiàn)了文章:Why is Babel a monorepo?抱既, 其中提到:
monorepo 的優(yōu)勢:
- 所有項(xiàng)目擁有一致的 lint职烧,以及構(gòu)建、測試、發(fā)布流程
- 不同項(xiàng)目之間容易調(diào)試蚀之、協(xié)作
- 方便處理 issues
- 容易初始化開發(fā)環(huán)境
- 易于發(fā)現(xiàn) bug
monorepo 的劣勢:
- 源代碼不易理解
- 項(xiàng)目體積過大
這些分析與我們前文提到的類似蝗敢。但是,從業(yè)內(nèi)技術(shù)發(fā)展來看足删,monorepo 目前越來越受歡迎寿谴。了解了 monorepo 的利弊,我們應(yīng)該如何實(shí)現(xiàn) monorepo 呢失受?
使用 Lerna 實(shí)現(xiàn) monorepo
Lerna 是 Babel 管理自身項(xiàng)目并開源的工具讶泰,官網(wǎng)對 Lerna 的定位非常簡單直接:
A tool for managing JavaScript projects with multiple packages.
我們來建立一個(gè)簡單的 demo,首先安裝依賴拂到,并創(chuàng)建項(xiàng)目:
mkdir new-monorepo && cd new-monorepo
npm init -y
npm i -g lerna(有需要的話要 sudo)
git init new-monorepo
lerna init
成功后痪署,Lerna 會在 new-monorepo 項(xiàng)目下自動添加以下三個(gè)文件目錄:
- packages
- lerna.json
- package.json
我們添加第一個(gè)項(xiàng)目 module-1:
cd packages
mkdir module-1
cd module-1
npm init -y
這樣,我們在 ./packages 目錄下新建了第一個(gè)項(xiàng)目:module-1兄旬,并在 module-1 中添加了一些依賴狼犯,模擬更加真實(shí)的場景。同樣的方式辖试,建立 module-2 以及 module-3辜王。
此時(shí),讀者可以自行觀察 new-monorepo 項(xiàng)目下的目錄結(jié)構(gòu)為:
packages/
module-1/
package.json
module-2/
package.json
module-3/
package.json
接下來罐孝,我們退到主目錄下,安裝依賴:
cd ..
lerna bootstrap
關(guān)于該命令的作用肥缔,官網(wǎng)直述為:
Bootstrap the packages in the current Lerna repo. Installs all of their dependencies and links any cross-dependencies.
也就是說莲兢,假設(shè)我們在 module-1 項(xiàng)目中添加了依賴 module-2,那么執(zhí)行 lerna bootstrap 命令后续膳,會在 module-1 項(xiàng)目的 node_modules 下創(chuàng)建軟鏈接直接指向 module-2 目錄改艇。也就是說 lerna bootstrap 命令會建立整個(gè)項(xiàng)目內(nèi)子 repo 之間的依賴關(guān)系,這種建立方式不是通過「硬安裝」坟岔,而是通過軟鏈接指向相關(guān)依賴谒兄。
Linux 中關(guān)于硬鏈接和軟鏈接的區(qū)別,可以參考文章:linux 硬鏈接與軟鏈接社付。
在正確連接了 Git 遠(yuǎn)程倉庫后承疲,我們可以發(fā)布:
lerna publish
這條命令將各個(gè) package 一步步發(fā)布到 npm 當(dāng)中。Lerna 還可以支持自動生成 changelog 等功能鸥咖。這里我們不再統(tǒng)一介紹燕鸽。
到這里,你可能覺得 Lerna 還挺簡單啼辣。但其實(shí)里面還是有更多學(xué)問啊研,比如 Lerna 支持下面兩種模式。
Fixed/Locked 模式
Babel 便采用了這樣的模式。這個(gè)模式的特點(diǎn)是党远,開發(fā)者執(zhí)行 lerna publish 后削解,Lerna 會在 lerna.json 中找到指定 version 版本號。如果這一次發(fā)布包含某個(gè)項(xiàng)目的更新沟娱,那么會自動更新 version 版本號氛驮。對于各個(gè)項(xiàng)目相關(guān)聯(lián)的場景,這樣的模式非常有利花沉,任何一個(gè)項(xiàng)目大版本升級柳爽,其他項(xiàng)目的大版本號也會更新。Independent 模式
不同于 Fixed/Locked 模式碱屁,Independent 模式下磷脯,各個(gè)項(xiàng)目相互獨(dú)立。開發(fā)者需要獨(dú)立管理多個(gè)包的版本更新娩脾。也就是說赵誓,我們可以具體到更新每個(gè)包的版本。每次發(fā)布柿赊,Lerna 會配合 Git俩功,檢查相關(guān)包文件的變動,只發(fā)布有改動的 package碰声。
開發(fā)者可以根據(jù)團(tuán)隊(duì)需求進(jìn)行模式選擇诡蜓。
我們也可以使用 Lerna 安裝依賴,該命令可以在項(xiàng)目下的任何文件夾中執(zhí)行:
lerna add dependencyName
Lerna 默認(rèn)支持 hoist 選項(xiàng)胰挑,即默認(rèn)在 lerna.json 中:
{ bootstrap: { hoist: true } }
這樣項(xiàng)目中所有的 package 下 package.json 都會出現(xiàn) dependencyName 包:
packages/
module-1/
package.json(+ dependencyName)
node_modules
module-2/
package.json(+ dependencyName)
node_modules
module-3/
package.json(+ dependencyName)
node_modules
node_modules
dependencyName
這種方式蔓罚,會在父文件夾的 node_modules 中高效安裝 dependencyName(Node.js 會向上在祖先文件夾中查找依賴)。對于未開啟 hoist 的情況瞻颂,執(zhí)行 lerna add 后豺谈,需要執(zhí)行:
lerna bootstrap --hoist
如果我們想有選擇地升級某個(gè)依賴,比如只想為 module-1 升級 dependencyName 版本贡这,可以使用 scope 參數(shù):
lerna add dependencyName --scope=module-1
這時(shí)候 module-1 文件夾下會有一個(gè) node_modules茬末,其中包含了 dependencyName 的最新版本。
分析一個(gè)項(xiàng)目遷移案例
接下來盖矫,我選取一個(gè)正在線上運(yùn)行的 multirepo 項(xiàng)目丽惭,并演示使用 Lerna 將其遷移到 monorepo 的過程。此案例來自 mitter.io炼彪,該團(tuán)隊(duì)以往一直以 multirepo 的形式維護(hù)以下幾個(gè)項(xiàng)目:
- @mitter-io/core吐根,mitter.io SDK 核心基礎(chǔ)庫
- @mitter-io/models,TypeScript models 庫
- @mitter-io/web辐马,Web 端 SDK 應(yīng)用
- @mitter-io/react-native拷橘,React Native 端 SDK 應(yīng)用
- @mitter-io/node局义,Node.js 端 SDK 應(yīng)用
- @mitter-io/react-scl,React.js 組件庫
背景介紹
項(xiàng)目使用 TypeScript 和 Rollup 工具冗疮,以及 TypeDoc 生成規(guī)范化文檔萄唇。在使用 Lerna 做 monorepo 化之前,這樣的技術(shù)方案帶來的困擾顯而易見术幔,我們來分析一下當(dāng)前技術(shù)棧的弊端另萤,以及 monorepo 化能為這些項(xiàng)目帶來哪些收益。
- 如果 @mitter-io/core 中出現(xiàn)任何一處改動诅挑,其他所有的包都需要升級到 @mitter-io/core 最新版本四敞,不管這些改動是 feature 還是 bug fix,成本都比較大
- 如果所有這些包能共同分享版本拔妥,那么帶來的收益也是非常巨大的
- 這些不同的倉庫之間忿危,由于技術(shù)棧近似,一些構(gòu)建腳本大體相同没龙,部署流程也都一致铺厨,如果能夠?qū)⑦@些腳本統(tǒng)一抽象,也將帶來便利
遷移步驟
我們運(yùn)用 Lerna 構(gòu)建 monorepo 項(xiàng)目硬纤,第一步:
mkdir my-new-monorepo && cd my-new-monorepo
git init .
lerna init
不同于之前的示例解滓,這是從現(xiàn)有項(xiàng)目中導(dǎo)入,因此我們可以使用命令:
lerna import ~/projects/my-single-repo-package-1 --flatten
這行命令不僅可以導(dǎo)入項(xiàng)目筝家,同時(shí)也會將已有項(xiàng)目中的 git commit 一并搬遷過來洼裤。我們可以放心地在新 monorepo 倉庫中使用 git blame 來進(jìn)行回溯。
如此一來溪王,得到了這樣的項(xiàng)目結(jié)構(gòu):
packages/
core/
models/
node/
react-native/
web/
lerna.json
package.json
接下來逸邦,運(yùn)行熟悉的:
lerna boostrap
lerna publish
進(jìn)行依賴維護(hù)和發(fā)布。注意并不是每次都需要執(zhí)行 lerna bootstrap在扰,只需要在第一次切換到項(xiàng)目,安裝所有依賴時(shí)運(yùn)行雷客。
對于每一個(gè) package 來說芒珠,其 pacakge.json 文件中都有以下雷同的 npm script 聲明。
"scripts": {
...
"prepare": "yarn run build",
"prepublishOnly": "./../../ci-scripts/publish-tsdocs.sh",
...
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src"
}
受益于 monorepo搅裙,所有項(xiàng)目得以集中管理在一個(gè)倉庫中皱卓,這樣我們將所有 package 公共的 npm 腳本移到 ./scripts 文件中。在單一的 monorepo 項(xiàng)目里部逮,我們就可以在不同 package 之間共享構(gòu)建腳本了 娜汁。
運(yùn)行公共腳本時(shí),有時(shí)候有必要知道當(dāng)前運(yùn)行的項(xiàng)目信息兄朋。npm 是能夠讀取到每個(gè) package.json 信息的掐禁。因此,對每個(gè) package,在其 package.json 中添加以下信息:
{
"name": "@mitter-io/core",
"version": "0.6.28",
"repository": {
"type": "git"
}
}
之后傅事,如下變量都可以被 npm script 使用:
npm_package_name = @mitter-io/core
npm_package_version = 0.6.28
npm_package_repository_type = git
流程優(yōu)化
團(tuán)隊(duì)中正常的開發(fā)流程是每個(gè)程序員新建一個(gè) git branch缕允,通過代碼審核之后進(jìn)行合并。整套流程在 monorepo 架構(gòu)下變得非常清晰蹭越,我們來梳理一下障本。
- step1:當(dāng)開發(fā)完成后,我們計(jì)劃進(jìn)行版本升級响鹃,只需要運(yùn)行:lerna version
- step2:Lerna 會提供交互式 prompt驾霜,對下一版本進(jìn)行序號升級
lerna version --force-publish
lerna notice cli v3.8.1
lerna info current version 0.6.2
lerna info Looking for changed packages since v0.6.2
? Select a new version (currently 0.6.2) (Use arrow keys)
? Patch (0.6.3)
Minor (0.7.0)
Major (1.0.0)
Prepatch (0.6.3-alpha.0)
Preminor (0.7.0-alpha.0)
Premajor (1.0.0-alpha.0)
Custom Prerelease
Custom Version
新版本被選定之后,Lerna 會自動改變每個(gè) package 的版本號买置,在遠(yuǎn)程倉庫中創(chuàng)建一個(gè)新的 tag粪糙,并將所有的改動推送到 GitLab 實(shí)例當(dāng)中。
接下來堕义,CI 構(gòu)建實(shí)際上只需要兩步:
- Build 構(gòu)建
- Publish 發(fā)布
構(gòu)建實(shí)際就是運(yùn)行:
lerna bootstrap
lerna run build
而發(fā)布也不復(fù)雜猜旬,需要執(zhí)行:
git checkout master
lerna bootstrap
git reset --hard
lerna publish from-package --yes
注意,這里我們使用了 lerna publish from-package倦卖,而不是簡單的 lerna publish洒擦。因?yàn)殚_發(fā)者在本地已經(jīng)運(yùn)行了 lerna version,這時(shí)候再運(yùn)行 lerna publish 會收到「當(dāng)前版本已經(jīng)發(fā)布」的提示怕膛。而 from-package 參數(shù)會告訴 Lerna 發(fā)布所有非當(dāng)前 npm package 版本的項(xiàng)目熟嫩。
通過這個(gè)案例,我們了解了 Lerna 構(gòu)建 monorepo 的經(jīng)典套路褐捻,Lerna 還封裝了更多的 API 來支持更加靈活的 monorepo 的創(chuàng)建掸茅,感興趣的讀者可以自行研究,歡迎在評論區(qū)留言討論柠逞,或者直接向我提問昧狮。個(gè)人認(rèn)為序无,未來 monorepo 和 multirepo 將會持續(xù)并存酥馍,每個(gè)開發(fā)者都應(yīng)該根據(jù)項(xiàng)目特點(diǎn)來進(jìn)行選擇。
到此帘饶,我們分析了 multirepo 和 monorepo 方案的各自特點(diǎn)绰精,通過實(shí)例和項(xiàng)目遷移了解了如何構(gòu)建 monorepo 項(xiàng)目撒璧。但是,項(xiàng)目組織不光這些內(nèi)容笨使,下一節(jié)我們將討論依賴關(guān)系這一話題卿樱。
總結(jié)
monorepo 目前來看是一個(gè)流行趨勢,筆者為項(xiàng)目團(tuán)隊(duì)引入了 monorepo 的架構(gòu)方案之后收益非常明顯硫椰,我們也是國內(nèi)最早采用 monorepo 架構(gòu)的團(tuán)隊(duì)之一繁调。
但是這篇課程難以做到面面俱到萨蚕,并且任何一個(gè)項(xiàng)目都有自己的獨(dú)立性和特殊性,究竟該如何組織調(diào)配涉馁、生產(chǎn)部署门岔,需要每一個(gè)開發(fā)者開動腦筋。
比如:monorepo 方式會導(dǎo)致整個(gè)項(xiàng)目體積變大烤送,在上線部署時(shí)寒随,用時(shí)更長,甚至難以忍受帮坚。在工程中如何解決這類問題妻往?針對于此,我設(shè)計(jì)了增量部署構(gòu)建方案试和,通過分析項(xiàng)目依賴以及拓?fù)渑判蜓镀瑑?yōu)化項(xiàng)目編譯構(gòu)建,這里不再多做介紹阅悍。
如果對工程化話題格外感興趣的讀者較多好渠,我會專門進(jìn)行講解。希望大家一起討論节视。