1. 程式人生 > >使用mono-repo實現跨專案元件共享

使用mono-repo實現跨專案元件共享

本文會分享一個我在實際工作中遇到的案例,從最開始的需求分析到專案搭建,以及最後落地的架構的整個過程。最終實現的效果是使用`mono-repo`實現了跨專案的元件共享。在本文中你可以看到: 1. 從接到需求到深入分析並構建架構的整個思考過程。 2. `mono-repo`的簡單介紹。 3. `mono-repo`適用的場景分析。 4. 產出一個可以跨專案共享元件的專案架構。 **本文產出的架構模板已經上傳到GitHub,如果你剛好需要一個mono-repo + react的模板,直接clone下來吧:[https://github.com/dennis-jiang/mono-repo-demo](https://github.com/dennis-jiang/mono-repo-demo)** ## 需求 ### 需求概況 是這麼個情況,我還是在那家外企供職,不久前我們接到一個需求:要給外國的政府部門或者他的代理機構開發一個可以繳納水電費,順便還能賣賣可樂的網站。主要使用場景是市政廳之類的地方,類似這個樣子: ![image-20201224162525774](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb4777294d4c4c8aa0548315c11a4a11~tplv-k3u1fbpfcp-zoom-1.image) 這張圖是我在網上隨便找的某銀行的圖片,跟我們使用場景有點類似。他有個自助的ATM機,遠處還有人工櫃檯。我們也會有自助機器,另外也會有人工櫃檯,這兩個地方都可以交水電費,汽車罰款什麼的,唯一有個區別是人工那裡除了交各種賬單,還可能會賣點東西,比如口渴了買個可樂,煙癮犯了來包中華。 ### 需求分析 上面只是個概況,要做下來還有很多東西需要細化,櫃員使用的功能和客戶自助使用的功能看起來差不多,細想下來區別還真不少: 1. 無論是交賬單還是賣可樂,我們都可以將它視為一個商品,既然賣商品那肯定有上架和下架的功能,也就是商品管理,這個肯定只能做在櫃員端。 2. 市政廳人員眾多,也會有上下級關係,普通櫃員可能沒有許可權上/下架,他可能只有售賣許可權,上/下架可能需要經理才能操作,這意味著櫃員介面還需要許可權管理。 3. 許可權管理的基礎肯定是使用者管理,所以櫃員介面需要做登陸和註冊。 4. 客戶自助介面只能交賬單不能賣可樂很好理解,因為是自助機,旁邊無人值守,如果擺幾瓶可樂,他可能會拿了可樂不付錢。 5. 那客戶自助交水電費需要登陸嗎?不需要!跟國內差不多,只需要輸入卡號和姓名等基本資訊就可以查詢到賬單,然後線上信用卡就付了。所以客戶介面不需要登陸和使用者管理。 從上面這幾點分析我們可以看出,櫃員介面會多很多功能,包括商品管理,使用者管理,許可權管理等,而客戶自助介面只能交賬單,其他功能都沒有。 ### 原型設計 基於上面幾點分析,我們的設計師很快設計了兩個介面的原型。 **這個是櫃員介面的**: ![image-20201224172006928](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/770694558eba46b6b5bd5e460bdec1d7~tplv-k3u1fbpfcp-zoom-1.image) 櫃員介面看起來也很清爽,上面一個頭部,左上角顯示了當前機構的名稱,右上角顯示了當前使用者的名字和設定入口。登陸/登出相關功能點選使用者名稱可以看到,商品管理,使用者管理需要點選設定按鈕進行跳轉。 **這個是客戶自助介面的**: ![image-20201224172649189](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcb3572c06c245e0ba4e82fcbff0b99c~tplv-k3u1fbpfcp-zoom-1.image) 這個是客戶介面的,看起來基本是一樣的,只是少了使用者和設定那一塊,賣的東西少了可樂,只能交賬單。 ## 技術 現在需求基本已經理清楚了,下面就該我們技術出馬了,進行技術選型和架構落地。 ### 一個站點還是兩個站點? 首先我們需要考慮的一個問題就是,櫃員介面和客戶介面是做在一個網站裡面,還是單獨做兩個網站?因為兩個介面高度相似,所以我們完全可以做在一起,在客戶自助介面隱藏掉右上角的使用者和設定就行了。 但是這裡面其實還隱藏著一個問題:**櫃員介面是需要登陸的,所以他的入口其實是登陸頁;客戶介面不需要登陸,他的入口應該直接就是售賣頁**。如果將他們做在一起,因為不知道是櫃員使用還是客戶使用,所以入口只能都是登入頁,櫃員直接登陸進入售賣頁,對於客戶可以單獨加一個“客戶自助入口”讓他進入客戶的售賣頁面。但是這樣**使用者體驗不好**,客戶本來不需要登陸的,你給他看一個登入頁可能會造成困惑,可能需要頻繁求教工作人員才知道怎麼用,會降低整體的工作效率,所以產品經理並不接受這個,要求客戶一進來就需要看到客戶的售賣頁面。 而且從技術角度考慮,現在我們是一個`if...else...`隱藏使用者和設定就行了,那萬一以後兩個介面差異變大,客戶介面要求更花哨的效果,就不是簡單的一個`if...else...`能搞定的了。**所以最後我們決定部署兩個站點,櫃員介面和客戶介面單獨部署到兩個域名上**。 ### 元件重複 既然是兩個站點,考慮到專案的可擴充套件性,我們建立了兩個專案。但是這兩個專案的UI在目前階段是如此相似,如果我們寫兩套程式碼,勢必會有很多元件是重複的,比較典型的就是上面的商品卡片,購物車元件等。其實除了上面可以看到這些會重複外,我們往深入想,交個水費,我們肯定還需要使用者輸入姓名,卡號之類的資訊,所以點了水費的卡片後肯定會有一個輸入資訊的表單,而且這個表單在櫃員介面和客戶介面基本是一樣的,除了水費表單外,還有電費表單,罰單表單等等,所以可以預見重複的元件會非常多。 作為一個有追求的工程師,這種重複元件肯定不能靠CV大法來解決,我們得想辦法讓這些元件可以複用。那元件怎麼複用呢?提個公共元件庫嘛,相信很多朋友都會這麼想。我們也是這麼想的,但是公共元件庫有多種組織方式,我們主要考慮了這麼幾種: #### 單獨NPM包 再建立一個專案,這個專案專門放這些可複用的元件,類似於我們平時用的`antd`之類的,建立好後釋出到公司的私有NPM倉庫上,使用的時候直接這樣: ```javascript import { Cart } from 'common-components'; ``` 但是,我們需要複用的這些元件跟`antd`元件有一個本質上的區別:**我們需要複用的是業務元件,而不是單純的UI元件**。`antd`UI元件庫為了保證通用性,基本不帶業務屬性,樣式也是開放的。但是我這裡的業務元件不僅僅是幾個按鈕,幾個輸入框,而是一個完整的表單,包括前端驗證邏輯都需要複用,**所以我需要複用的元件其實是跟業務強繫結的**。因為他是跟業務強繫結的,即使我將它作為一個單獨的NPM包釋出出去,公司的其他專案也用不了。一個不能被其他專案共享的NPM包,始終感覺有點違和呢。 #### git submodule 另一個方案是`git submodule`,我們照樣為這些共享元件建立一個新的Git專案,但是不釋出到NPM倉庫去騷擾別人,而是直接在我們主專案以`git submodule`的方式引用他。`git submodule`的基本使用方法網上有很多,我這裡就不囉嗦了,主要說幾個缺點,也是我們沒采用他的原因: 1. 本質上`submodule`和主專案是兩個不同的`git repo`,所以你需要為每個專案建立一套腳手架(程式碼規範,釋出指令碼什麼的)。 2. `submodule`其實只是主專案儲存了一個對子專案的依賴連結,說明了當前版本的主專案依賴哪個版本的子專案,你需要小心的使用`git submodule update`來管理這種依賴關係。如果沒有正確使用`git submodule update`而搞亂了版本的依賴關係,那就呵呵了。。。 3. 釋出的時候需要自己小心處理依賴關係,先發子專案,子專案好了再發布主專案。 #### mono-repo `mono-repo`是現在越來越流行的一種專案管理方式了,與之相對的叫`multi-repo`。`multi-repo`就是`多個倉庫`,上面的`git submodule`其實就是`multi-repo`的一種方式,主專案和子專案都是單獨的`git倉庫`,也就構成了`多個倉庫`。而`mono-repo`就是`一個大倉庫`,多個專案都放在`一個git倉庫`裡面。現在很多知名開源專案都是採用的`mono-repo`的組織方式,比如`Babel`,`React` ,`Jest`, `create-react-app`, `react-router`等等。`mono-repo`特別適合聯絡緊密的多個專案,比如本文面臨的這種情況,下面我們就進入本文的主題,認真看下`mono-repo`。 ## mono-repo 其實我之前寫[`react-router`原始碼解析的時候就提到過`mono-repo`](https://juejin.cn/post/6855129007949398029#heading-2),當時就說有機會單獨寫一篇`mono-repo`的文章,本文也算是把坑填上了。所以我們先從`react-router`的原始碼結構入手,來看下`mono-repo`的整體情況,下圖就是`react-router`的原始碼結構: ![image-20201225153108233](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1e9bbf33c31d4eec881dd70ed19c03f2~tplv-k3u1fbpfcp-zoom-1.image) 我們發現他有個`packages`資料夾,裡面有四個專案: 1. **react-router**:是`React-Router`的核心庫,處理一些共用的邏輯 2. **react-router-config**:是`React-Router`的配置處理庫 3. **react-router-dom**:瀏覽器上使用的庫,會引用`react-router`核心庫 4. **react-router-native**:支援`React-Native`的路由庫,也會引用`react-router`核心庫 這四個專案都是為`react`的路由管理服務的,在業務上有很強的關聯性,完成一個功能可能需要多個專案配合才能完成。比如修某個BUG需要同時改`react-router-dom`和`react-router`的程式碼,如果他們在不同的Git倉庫,需要在兩個倉庫裡面分別修改,提交,打包,測試,然後還要修改彼此依賴的版本號才能正常工作。但是使用了`mono-repo`,因為他們程式碼都在同一個Git倉庫,我們在一個`commit`裡面就可以修改兩個專案的程式碼,然後統一打包,測試,釋出,如果我們使用了`lerna`管理工具,版本號的依賴也是自動更新的,實在是方便太多了。 ### lerna `lerna`是最知名的`mono-repo`的管理工具,今天我們就要用它來搭建前面提到的共享業務元件的專案,我們目標的專案結構是這個樣子的: ```bash mono-repo-demo/ --- 主專案,這是一個Git倉庫 package.json packages/ common/ --- 共享的業務元件 package.json admin-site/ --- 櫃員網站專案 package.json customer-site/ --- 客戶網站專案 package.json ``` ### lerna init `lerna`初始化很簡單,先建立一個空的資料夾,然後執行: ```javascript npx lerna init ``` 這行命令會幫我建立一個空的`packages`資料夾,一個`package.json`和`lerna.json`,整個結構長這樣: ![image-20201225162905950](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/719c317112f74794bc77fc1ddc43a7f0~tplv-k3u1fbpfcp-zoom-1.image) `package.json`中有一點需要注意,他的`private`必須設定為`true`,因為`mono-repo`本身的這個Git倉庫並不是一個專案,他是多個專案,所以他自己不能直接釋出,釋出的應該是`packages/`下面的各個子專案。 ```json "private": true, ``` `lerna.json`初始化長這樣: ```json { "packages": [ "packages/*" ], "version": "0.0.0" } ``` `packages`欄位就是標記你子專案的位置,預設就是`packages/`資料夾,他是一個數組,所以是支援多個不同位置的。另外一個需要特別注意的是`version`欄位,這個欄位有兩個型別的值,一個是像上面的`0.0.0`這樣一個具體版本號,還可以是`independent`這個關鍵字。如果是`0.0.0`這種具體版本號,那`lerna`管理的所有子專案都會有相同的版本號----`0.0.0`,如果你設定為`independent`,那各個子專案可以有自己的版本號,比如子專案1的版本號是`0.0.0`,子專案2的版本號可以是`0.1.0`。 ### 建立子專案 現在我們的`packages/`目錄是空的,根據我們前面的設想,我們需要建立三個專案: 1. `common`:共享的業務元件,本身不需要執行,放各種元件就行了。 2. `admin-site`:櫃員站點,需要能夠執行,使用`create-react-app`建立吧 3. `customer-site`:客戶站點,也需要執行,還是使用`create-react-app`建立 建立子專案可以使用`lerna`的命令來建立: ```bash lerna