使用 MonoRepo 管理前端項目
前言
在工作中,我們可能會遇到一些項目管理方面的問題。在單個項目管理的時候,大家都知道該怎么管理。一旦涉及到多個項目管理,很多人就不一定能夠管理好了。
這篇文章主要講解一下 monorepo 在我們團隊的應(yīng)用。
multi-repo 的困境
在通常情況下,我們新開一個項目會先在 Github 上面創(chuàng)建一個新倉庫,然后在本地創(chuàng)建這個項目,和遠(yuǎn)程倉庫進(jìn)行關(guān)聯(lián),基本上是一個倉庫對應(yīng)一個項目。
復(fù)用代碼和配置困難
一旦項目多起來,就會遇到一些更復(fù)雜的情況。比如一些獨立的 h5 活動頁面,這些頁面往往是不相關(guān)的,不方便部署到一起,需要獨立部署到不同域名。
除此之外,這些頁面可能會有很多共同之處,比如同樣的錯誤處理、同樣的多語言文案、同樣的 eslint 和 prettier 處理等等。
如果有腳手架倒也還好,直接創(chuàng)建一個新項目就行了。但很多團隊也沒有維護(hù)腳手架,每次新開一個項目就是把原來項目的配置給復(fù)制粘貼過去,做一些修改,這樣效率非常低下。
資源浪費
同時,每次有一個新的頁面就去創(chuàng)建一個項目,這些項目也會過于分散,不便管理。
還會白白浪費資源,比如它們可能都會安裝 React、React-dom 等包,不小心就造成了杯具(一個 node_module 有多大心里沒數(shù)嗎)。

調(diào)試麻煩
如果你想在本地項目進(jìn)行調(diào)試,但這個項目依賴了另一個項目,那么你只能用 npm link 的方式將它 link 到需要調(diào)試的項目里面。
一旦 link 的項目多了,手動去管理這些 link 操作就容易心累,進(jìn)一步就會發(fā)展到摔鍵盤、砸顯示器。
npm 和 submodules
你可能會想著把它們發(fā)布到 npm,可一旦有一個新的版本變更,每個依賴的項目都要跟著改。
那么有什么好辦法呢?也許你還會想到 git submodules,把這些相同的部分放到 git 倉庫里面,通過 submodules 的形式來集成進(jìn)來。

submodules 確實可以解決這個問題,但還不足以解決前面說的重復(fù)安裝依賴的問題。而且submodules 這玩意有多難用,懂得都懂。
其實在我們這邊,倒是有一種很合適 submodules 的使用場景。
我們這里的服務(wù)都是用 go 和 grpc 寫的,但是前端無法直接調(diào)用 grpc 接口(雖然有庫可以支持,但體積實在太大了)。
所以就需要一個 Node 服務(wù)去動態(tài)加載 .proto 文件來調(diào)用 grpc 服務(wù),這層 Node 服務(wù)起到將 grpc 接口轉(zhuǎn)換到 http 接口的作用,我們稱之為網(wǎng)關(guān)。

這樣,在服務(wù)里面,我們必然要拿到后端的 proto 文件才能動態(tài)調(diào)用服務(wù),所以可以直接把后端的 proto 文件用 submodules 的形式嵌入項目里面。
這樣我們不需要手動 copy 他們的文件到項目中,他們每次改動我們只需要更新一下 submodule 就行了。
monorepo
monorepo 和 multirepo 是相反的兩個概念。monorepo 允許我們將多個項目放到同一個倉庫里面進(jìn)行管理。

目前很多開源項目都用了 monorepo,比如 babel、nuxtjs 都使用了 lerna 來管理項目。
lerna 用起來也比較簡單,只要使用 lerna init 就可以生成 packages 目錄和 lerna.json 文件。
lerna 項目的結(jié)構(gòu)目錄一般如下:
-?packages
??-?project1
????-?src
??????-?index.ts
????-?package.json
??-?project2
????-?src
??????-?index.ts
????-?package.json
??-?project3
????-?src
??????-?index.ts
????-?package.json
-?lerna.json
-?package.json
-?tsconfig.json
lerna 和 yarn workspace 在功能上大同小異,這里主要講 yarn workspace 的用法(因為 lerna 我真的用的不多?。?/p>
yarn workspace
workspaces 是 yarn 相對 npm 的一個重要優(yōu)勢(另一個優(yōu)勢是下載更快),它允許我們使用 monorepo 的形式來管理項目。
開啟 workspace 的功能也比較簡單,只需要在 package.json 里面將 private 設(shè)置為 true,并且規(guī)定好 workspaces 字段里面的子項目就好了。
以上面 lerna 的項目結(jié)構(gòu)為例:
{
????...
????private:?true,
????workspaces:?[
??????"packages/*"
????]
}
當(dāng)然,yarn workspace 沒有規(guī)定你一定要放到 packages 目錄下面。你也可以不使用通配符,直接手動聲明每個子項目。
{
????...
????private:?true,
????workspaces:?[
??????"packages/project1",
??????"packages/project2"
????]
}
在安裝 node_modules 的時候它不會安裝到每個子項目的 node_modules 里面,而是直接安裝到根目錄下面,這樣每個子項目都可以讀取到根目錄的 node_modules。
整個項目只有根目錄下面會有一份 yarn.lock 文件。子項目也會被 link 到 node_modules 里面,這樣就允許我們就可以直接用 import 導(dǎo)入對應(yīng)的項目。
-?node_modules
??-?project1
??-?project2
??-?project3
-?packages
??-?shared
????-?src
??????-?index.ts
??-?project1
????-?node_modules
????-?src
??????-?index.ts
????-?package.json
??-?project2
????-?node_modules
????-?src
??????-?index.ts
????-?package.json
??-?project3
????-?node_modules
????-?src
??????-?index.ts
????-?package.json
-?yarn.lock
-?package.json
-?tsconfig.json
當(dāng)然,如果你的子項目里面依賴了不同版本的包,那么也會在子項目的 node_modules 里面安裝對應(yīng)版本的包。
比如根目錄的 package.json 里面是 2.5 的 vue,而 project1 里面安裝了 2.6 的 vue,那么就會在根目錄的 node_modules 里面安裝 2.5 版本,而 project 下面的 node_modules 安裝 2.6 版本。
-?node_modules
[email protected]
-?packages
??-?shared
????-?src
??????-?index.ts
??-?project1
????-?node_modules
[email protected]
????-?src
??????-?index.ts
????-?package.json
??-?project2
????-?node_modules
????-?src
??????-?index.ts
????-?package.json
??-?project3
????-?node_modules
????-?src
??????-?index.ts
????-?package.json
-?yarn.lock
-?package.json
-?tsconfig.json
如果多個子項目依賴了同一個包的不同版本,那么根目錄里面安裝的就是版本號最高的那個。
yarn workspace 命令
yarn workspace 提供了一些常用的命令。
一般來說,執(zhí)行某個項目下面的某個命令都用 yarn workspace project run xxx。
執(zhí)行所有項目下面的某個命令要用 yarn workspaces run xxx。
安裝依賴
安裝整個項目的依賴和常規(guī)的 yarn 用法一樣,直接 yarn install 就完事了。
如果你想安裝一個依賴,那么分下面三種場景:
yarn workspaces add package:給所有應(yīng)用都安裝依賴 yarn workspace project add package:給某個應(yīng)用安裝依賴 yarn add -W -D package:給根應(yīng)用安裝依賴
清理 node_modules
如果想刪除所有的 node_modules,可以用 lerna clean,或者安裝 rimraf,然后在每個子項目的 package.json 里面寫上:
clean:?rimraf?node_modules
這樣你就可以在根目錄下面直接執(zhí)行 yarn workspaces run clean 來刪除所有項目的 node_modules 了。
也許你只是想重裝 node_modules,那么你可以用 yarn install --force 來重新獲取所有的 node_modules。
適用場景
零散的頁面
這種場景就是前面說過的一些 H5 活動頁,他們可能都依賴了 React、React-dom 等等,但又需要部署到不同的域名下面,這樣就不方便用 React-router 來管理了。
所以這里我們就可以將他們放到同一個倉庫里面,用 monorepo 的形式來管理這個倉庫。
由于他們使用了相同的技術(shù)棧,那么 eslint、prettier,甚至 webpack 配置都可以提取到最外面,不用維護(hù)在每個項目里面。
以 create-react-app eject 之后的配置為例:
-?node_modules
??-?react
??-?react-dom
??-?redux
??-?lodash
??
-?packages
??-?project1
????-?package.json
??-?project2
????-?package.json
????
-?config
??-?webpack.config.js
??-?webpack.dev.config.js
??
-?scripts
??-?create.js
??-?bin.js
??-?build.js
??-?start.js
??
-?.eslintrc.js
-?.prettierrc
-?commitlint.config.js
-?jest.config.js
-?tsconfig.js
-?package.json
-?yarn.lock
我們可以看到,通用配置都被提取到了最外層。
如果運行或者構(gòu)建子項目,只需要在子項目的 package.json 里面這么配置。在外面執(zhí)行 `yarn workspace project run build` 就行了。
?"start":?"node?../../scripts/start.js",
?"build":?"node?../../scripts/build.js",
?"test":?"node?../../scripts/test.js前后端項目
有時候我們需要用 NodeJS 為自己開發(fā)的前端項目寫一些簡單接口,常常需要創(chuàng)建一個 server 項目,但這個項目功能很簡單,也只有這個前端項目用。
那我們就不必把他們用兩個倉庫來管理,可以直接放到同一個倉庫管理。
-?website
??-?package.json
-?server
??-?package.json
-?package.json
在構(gòu)建的時候,可以直接用 server 去渲染 website 最后構(gòu)建出來的 index.html,這樣只需要配置一份 nginx 就行了。
一個栗子
最近剛好和隔壁團隊合作開發(fā)一個谷歌登錄的功能,我這邊提供登錄頁面和鑒權(quán)接口。
雖然這個功能是給他們用的,但后續(xù)也有可能接入其他團隊的應(yīng)用,所以我希望這個接口是通用的。
前期為了方便,我們兩邊的項目就先放到一起部署。但我的項目以后有可能會遷出去,所以我的服務(wù)這里就只導(dǎo)出了一個路由,他們的服務(wù)會動態(tài)加載我的路由。
所以項目結(jié)構(gòu)就變成了這樣:
-?login
-?website
-?loginServer
-?server
-?package.json
在 server 里面直接引入了 loginServer:
import?fastify?from?'fastify'
import?loginServer?from?'loginServer';
fastify.register(loginServer,?{
??prefix:?'/login'
})
在 loginServer 的 src/index.ts 里面,直接導(dǎo)出了一個路由。
import?routes?from?'./routes'
export?default?routes
這種組織結(jié)構(gòu)也是對 monorepo 的一種靈活使用。
總結(jié)
monorepo 雖然不是一門新技術(shù),但在接觸到之后就愛不釋手了,現(xiàn)在團隊里面很多項目都用 monorepo 的形式來管理。
配合團隊內(nèi)基于 Jenkins Groovy 實現(xiàn)的類 Github Actions 語法,在構(gòu)建部署上面也沒遇到什么困難。
感興趣的可以參考我的這個項目:https://github.com/yinguangyao/tinger
