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

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

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

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

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