大家好,我是若川。我持續組織了近一年的源碼共讀活動,感興趣的可以?點此掃碼加我微信?lxchuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北
籍前端群,可加我微信進群。
背景
目前云音樂內有多個RN收銀臺場景分布在不同的工程,比如頁面收銀臺,浮層收銀臺,個性收銀臺等,后續可能還會有別的收銀臺場景。
那在開發過程中存在的問題就是每個收銀臺的核心邏輯如商品展示、支付方式展示、下單購買等邏輯都大致相同,而每次有修改或者新增需求的時候都需要開發多次,重復代碼較多效率低下。
雖然可以通過發npm包的形式復用代碼,但是有些組件和代碼塊不太好抽成包,還會帶來調試麻煩,發版等問題。所以為了提高代碼復用,提高開發效率,我們希望能夠在一個倉庫內包含多個工程,也就是Monorepo形式。
Monorepo
什么是Monorepo
Monorepo是一種將多個項目的代碼集中在同一個倉庫中的軟件開發策略,與之對立的是傳統的MultiRepo策略,即每個項目在一個單獨的倉庫進行管理。目前像社區內一些著名的開源項目Babel、React和Vue等都是用這種策略來管理代碼。
Monorepo解決的問題
要想知道Monorepo解決了哪些問題與其優勢,我們先來看下MultiRepo存在的問題。
當我們在MultiRepo下兩個工程之前需要復用一些代碼時,往往會采用抽成npm包的形式。但當npm包有改動時我們需要做以下事情:
修改npm包代碼,通過npm link與兩個工程調試
調試完成后發布新版本
兩個工程升級npm包新版本,再進行發布
整個流程可以看出還是比較繁瑣的,那如果是在Monorepo下我們可以將公共部分抽成一個workspace,我們的兩個工程分別也是workspace可以直接引用公共workspace的代碼,工具會幫我們管理這些依賴關系,開發過程中調試起來也非常方便,而且不涉及到發包,版本依賴等,公共部分代碼改動完成后兩個工程部署即可。
從上述可以看出Monorepo主要有代碼復用容易、調試方便和簡化依賴管理等優點,這也是我們選擇這個方案的原因。
當然Monorepo也有一些缺點,比如:倉庫體積大、工程權限不好控制等。所以不管是Monorepo還是MultiRepo都不是完美的方案,只要能解決當下的問題就是好方案。
Monorepo的工具
目前業界最常見的實現monorepo工具和方案有lerna、yarn workspace和pnpm等。
Lerna
lerna是一個通過使用git和npm來優化多包倉庫管理工作流的工具,多用于多個npm包相互依賴的大型前端工程,提供了許多CLI命令幫助開發者簡化從npm開發,調試到發版的整個流程。但是目前已官宣停止維護。
Pnpm
pnpm是一個新型的依賴包管理工具,并支持workspace功能,它的優勢主要是通過全局存儲和硬鏈接來節省磁盤空間并提升安裝速度,通過軟鏈接來解決幻影依賴問題。但是RN的構建工具metro對于符號鏈接的解析還存在問題需要改造,成本較大。
Yarn workspace
yarn workspace是yarn提供的Menorepo依賴管理機制,是一個底層的工具,用于在倉庫根目錄下管理多個package的依賴,天然支持hoist功能,安裝依賴時會將packages中相同的依賴提升到根目錄,減少重復依賴安裝。workspace之間的引用在依賴安裝時通過yarn link建立軟鏈,代碼修改時可以在依賴其的workspace中實時生效,調試方便。
通常業界主流方案是lerna + yarn worksapce,lerna負責發布和版本升級,yarn workspace負責依賴管理。因為我們的RN工程是頁面工程,不涉及到發npm包,而且需要依賴提升的功能(這個后面會說到),所以最終采用yarn worspace方案。
Metro
在工程改造之前,我們先了解下ReactNative的構建工具Metro。
Metro在構建過程中主要會經歷三個階段:
Resolution:此階段Metro會從入口文件出發分析所依賴的模塊生成一個所有模塊的依賴圖,主要是使用jest-haste-map這個包做依賴分析。這個階段和Transformation階段是并行的;
Transformation:此階段主要是將模塊代碼轉換成目標平臺可識別的格式;
Serialization:此階段主要是將Transform后的模塊進行序列化,然后組合這些模塊生成一個或多個Bundle
jest-haste-map是單元測試框架Jest的其中一個包,主要用來獲取監聽的所有文件及其依賴關系。
工程改造
接下來就是對工程的改造,首先我們將兩個RN工程放在一個工程下,并按照yarn workspace的方式進行配置,然后通過腳手架(這里使用的是公司內部自研的腳手架)分別創建app-a和app-b兩個RN工程,如下所示
rn-mono
|--?apps|--?app-a|--?app-b
|--?package.json
//?package.json
{..."workspaces":?{"packages":?["apps/*"]},"private":?true
}
接著我們運行
yarn?install
發現packages中相同的依賴都會安裝在根目錄下的node_modules中,接著我們用如下啟動app-a或app-b
yarn?workspace?app-a?run?dev
這時如果你的app-a工程中的dev啟動命令是用相對路徑的方式可能會出現命令找不到的情況,比如
//?app-a/package.json
{//?這里的react-native是安裝在了根目錄,所以會找不到命令,需要修改下路徑"script":?{"dev":?"node?./node_modules/react-native/local-cli/cli.js?start"}
}
那如果是調用./node_modules/.bin
中的命令則不需要,因為在安裝依賴的時候packages中.bin
中的命令會有個軟鏈指向根目錄下./node_modules/.bin
中的命令。啟動成功后,這時打開頁面會報如下錯誤:

這是因為jest-haste-map在做依賴分析時通過metro.config.js中的watcherFolders配置項來指定需要監聽變化的文件目錄。

watcherFolders默認值為工程根目錄,此時也就是app-a中目錄,但是我們的模塊都是安裝在根目錄下,所以會找不到。我們需要修改下metro.config.js中watcherFolders
//?app-a/metro.config.jsconst?path?=?require('path');module.exports?=?{watchFolders:?[path.resolve(__dirname,?'../../node_modules')],
};
修改完成后我們重新啟動,再打開頁面后發現已經可以正常打開了,同樣的方式app-b也可以正常運行。
但是我們對工程進行monorepo改造的目的是為了抽離公共組件,復用代碼。所以我們在根目錄下建立個common的文件夾來存放公共部分,此時根目錄下的pacage.json中的packages和apps里每個app的metro.config.js中watchFolder配置都需要加入common
rn-mono
|--?common|--?package.json
|--?apps|--?app-a|--?app-b
|--?package.json
//?package.json
{..."workspaces":?{"packages":?["apps/*","common"],},"private":?true
}//?apps/app-a/metro.config.js
const?path?=?require('path');module.exports?=?{watchFolders:?[path.resolve(__dirname,?'../../node_modules'),?path.resolve(__dirname,?'../../common')],
};
接著在common中添加個Button組件,package.json中添加相應的依賴,版本要和apps中對應依賴的版本保持一致
{..."dependencies":?{"react":?"16.8.6","react-native":?"0.60.5",},
}
然后yarn install重新安裝下,這時在根目錄的node_modules下就可以看到common模塊軟鏈到了common目錄,所以在app-a中引入common時就可以像npm包一樣直接引入,同樣app-b也可以。
import?common?from?'common';
到這里我們RN工程的monorepo改造也基本完成了。
依賴提升
這里解釋下為什么需要依賴提升。
我們先來看下取消依賴提升會有什么問題,可以在根目錄中的package.json中nohoist配置來指定不需要提升安裝到根目錄的模塊
{..."workspaces":?{"packages":?["apps/*","common"],"nohoist":?["**react**"],},"private":?true
}
然后重新yarn install,啟動app-a后會發現報如下錯誤

這是因為有些模塊jest-haste-map在做依賴分析生成dependency graph時發現在兩個不同的目錄下會產生命名沖突,導致報錯。所以我們需要依賴提升,將所用到的相同依賴安裝到根目錄,這樣只會安裝一次。
相同依賴的版本保持一致
雖然有了依賴提升但如果每個packages中相同依賴的版本不一致,同樣會導致相同的依賴會安裝多次的情況出現,根目錄和對應的package中都會有。這種情況除了會產生以上問題外還有可能產生其他潛在的問題,比如依賴客戶端的第三方模塊,如果存在多個版本在bundle執行時會多次注冊組件導致組件注冊失敗,在調用時會發生找不到組件的報錯。
雖然可以在metro中配置blacklistRE和extraNodeModules來表明要讀取哪個位置的依賴,但是這種方式并不通用,每次在引入新的依賴時都要去配置下較為繁瑣。所以我們需要將每個packages中的依賴版本保持一致。
人為的去約定這個規則肯定是不安全的,可以開發一個依賴版本的lint檢測工具,在提交代碼的時候做強制性的檢測。
我們最終的方案是開發一個檢測腳本結合gitlab-ci在分支代碼push的時候檢測,未通過則不允許push代碼來避免風險。
//?.gitlab-ci.yml
test-dev-version:stage:?testbefore_script:-?npm?install?--registry?http://rnpm.hz.netease.comscript:-?npm?run?depVerLintonly:changes:-?"package.json"-?"packages/**/package.json"

工程遷移過渡
如果是將多個正在快速迭代的工程遷移到一個Monorepo倉庫時,肯定會遇到存量開發分支代碼同步問題。比如我們要將工程A遷移到新倉庫,如果我們只是基于master分支將代碼copy到新工程,并在改造開發過程中還有組內其他同學也在基于master拉取分支做開發,并在你改造完成前開發完成合并到了master,此時你新工程的代碼是落后的,要想同步只能手動copy改動的代碼,很容易出錯。為了解決這個問題我們可以使用git subtree。git subtree允許將一個倉庫作為子倉庫嵌套在另一個倉庫里,所以這里我們可以將工程A作為一個子工程添加到Monorepo新工程對應的packages目錄下,如果有更新可以直接使用pull進行同步。
#?添加
git?subtree?add?--prefix=apps/app-a?https://github.com/xxxx/app-a.git?master?--squash#?更新
git?subtree?pull?--prefix=apps/app-a?https://github.com/xxxx/app-a.git?master?--squash
對于新工程或者新的開發分支就可以直接此工程下進行開發了。
構建
由于我們的構建機還不支持yarn,所以直接使用yarn workspace的命令是有問題的。目前的做法是將yarn作為devDependency,然后在根目錄下創建個腳本文件,將每個package的構建命令收斂在一起。結合yarn workspace的命令,這樣只需要在構建時傳入不同的package name即可。
##?scripts/build.shPLATFORM=$1
PROJECT=$2
EXEC_PARAMS=${@:2}
YARN="${PWD}/node_modules/.bin/yarn"...echo?"start?yarn?install"
${YARN}?cache?clean
${YARN}?installecho?"start?build"
echo?"${YARN}?workspace?${PROJECT}?run?build:${PLATFORM}?${EXEC_PARAMS}"
${YARN}?workspace?${PROJECT}?run?build:${PLATFORM}?${EXEC_PARAMS}
//?package.json
{..."workspaces":?{"packages":?["apps/*"],},"private":?true,"scripts":?{"build":?"./script/build.sh"},
}
比如對app-a進行構建,就可以
npm?run?build?ios?app-a##?實際上執行的是yarn?workspace?app-a?run?build:ios
總結
至此對React Native工程的monorepo改造基本完成了,對于多個功能類似的工程采用Monorepo的管理方式確實會方便代碼復用和調試,提高我們的開發效率。如果公司內部其余場景有類似的需求,未來規劃可以將其沉淀出一個腳手架。
目前對于h5工程的Monorepo方案已經較為成熟了,但是對RN工程來說由于構建機制不同無法完全適用,可參考的資料也較少。本文也是通過實踐記錄了一些踩坑經驗,如果你有更好的實踐,歡迎留言一起討論。