Monorepo?來聊聊另一種專案管理架構吧!使用 Vite+ pnpm 建立 Vue3 Monorepo

前言

Monorepo 是什麼呢?而這個專案架構又與普通的專案架構有什麼不同呢?剛好最近有相關專案的需求,所以就寫這一篇記錄一下。

什麼是 Monorepo?

Monorepo 是什麼東西呢?首先這是多個單字的組合,分別是 Mono 與 Repo,Mono 代表單一的意思,而 Repo 則是 Repository 的縮寫,也就是說 Monorepo 代表的是單一的 Repository。

感覺滿難懂的吧?沒關係,讓我們先講講其他的專案架構,接著我們再來 Monorepo 吧!

Monolith Repository(單體儲存庫)

通常我們在開發專案的時候,都會歷經以下幾個步驟:

  • 建立專案
  • 初始化版本控制(git init
  • 開發功能
  • 提交變更到遠端儲存庫(GitHub、GitLab、Bitbucket 等)

Repository

而這個專案就會獨立一個 Git Repository,也是我們最基本的專案架構,那麼這種架構又稱之為 Monolith Repository(後面僅稱 Monolith),也就是單一專案的 Repository。

那麼 Monolith 的專案架構可能長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- Repository
- .git
- README.md
- package.json
- src
- components
- Button
- Input
- ...
- pages
- index
- shopping
- admin
- ...
- public
- ...其他檔案

透過上面架構我們可以看到會 Monolith 會是一個 Git 管理一整個專案。

雖然 Monolith 的架構非常方便也簡單,在做一些 MVP 專案是相當不錯的首選,可是也會有一些問題存在,當專案開始越來越大時,這個專案就會變得越來越難維護,為什麼呢?舉例來講,我們這個專案裡面可能有包含了以下幾個功能模組:

  • 前台首頁
  • 前台購物頁面
  • 後台管理
    …等等功能

開發以及部署的速度會隨著專案越來越龐大而變得越來越慢,就連一個小改動你都需要跑完一整個專案的編譯、測試、部署等等流程,而且只要一個地方出錯,這個專案就會跟你說一聲「掰掰」

掰掰

那麼為了解決這問題,我們就延伸出了另一個專案架構,也就是 Multi Repository。

Multi Repository(多個儲存庫)

Multi Repository(後面簡稱 Multi)是什麼呢?簡單來講就是將原本的 Monolith 拆分成多個 Repository,也就是將原本的專案拆分成多個專案,每個專案都有自己的 Repository,這樣就可以讓每個專案獨立開發、獨立部署,這樣就可以解決 Monolith 的問題。

那麼這個專案架構就會從原本的 Monolith 變成以下的架構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- A Repository
- .git
- README.md
- package.json
- src
- components
- Button
- Input
- ...
- pages
- index
- ...
- public
- ...其他檔案
- B Repository
- .git
- README.md
- package.json
- src
- components
- Button
- Input
- ...
- pages
- shopping
- ...
- public
- ...其他檔案
- C Repository
- .git
- README.md
- package.json
- src
- components
- Button
- Input
- ...
- pages
- admin
- ...
- public
- ...其他檔案

看起來這種專案架構是相當不錯的,讓我們解決了牽一髮而動全身的問題,而這個專案架構也會將功能模組各自拆分成一個專案,這樣就可以讓每個專案獨立開發、獨立部署,這樣就可以解決 Monolith 的問題。

但卻也有一些問題發生,我們在上面的例子中,我們可以看到 Button、Input 這兩個功能模組都是重複的,這樣就會造成重複開發的問題,而且當這兩個功能模組有一個地方出錯時,我們就需要到每個專案去修改,這樣就會造成維護上的困難,當然不只有這個問題存在,像是專案的 CI/CD、套件的版本等等也就會變得更加難以維護 QAQ…

崩潰

Mono Repository(單一儲存庫)

那麼為了解決這個問題,所以又延伸出了另一個專案架構,也就是我們這一篇要說的重點 Monorepo 架構。

這時候應該有點混亂了,所以我決定畫圖來讓大家更加了解彼此的架構差異,首先是 Monolith:

Monolith Repository

我們可以看到 Monolith 的專案架構是一個專案一個 Repository,裡面通常會包含了許多功能模組,這樣就會造成牽一髮而動全身的問題。

接下來是 Multi:

Multi Repository

我們可以看到 Multi 的專案架構是將原本的 Monolith 拆分成多個 Repository,這樣就可以讓每個專案獨立開發、獨立部署,這樣就可以解決 Monolith 的問題。

最後是 Mono Repository(後面簡稱 Monorepo):

Mono Repository

透過圖像我們可以看到原本 Multi 都是獨立的 Git 管理,但是在 Monorepo 中,我們可以看到所有的專案都是在同一個 Repository 中並且由一個 Git 管理,這樣就可以解決 Multi 的問題,讓我們可以將所有的專案都放在同一個 Repository 中,讓所有的專案共享相同的資源。

雖然 Monorepo 的架構解決了 Multi 的問題,但也有一些缺點,例如…

  • 專案肥大後 git clone 時會花費較多時間
  • 共用的東西必須要規劃好,否則容易互相影響
  • 由於 Monorepo 中的專案都是共用的,所以無法區分專案的權限
  • Git 儲存庫的大小會變得非常大

我們可以發現不論是哪一種專案架構,都有屬於他的優缺點,所以在選擇專案架構時,可以評估一下自己的專案需求,再來選擇適合的專案架構。

用 Vite 實作一個 Monorepo

接下來我將會介紹如何使用 Vite + pnpm 建立一個 Vue3 的 Monorepo,首先請你先依照以下步驟準備好環境:

1
2
3
mkdir monorepo-example-pnpm-vue3
cd monorepo-example-pnpm-vue3
pnpm init

接下來我們要做一點特別的設定,在 pnpm 的 工作空間(Workspace) 官方文件有說到一件事情,如果我們要建立 Monorepo 的話會需要在根目錄建立一個「pnpm-workspace.yaml」檔案,所以請你在這個專案底下建立一個「pnpm-workspace.yaml」檔案。

Note
pnpm-workspace.yaml 這個檔案主要是用來告訴 pnpm 這個專案是一個 Monorepo 專案,並且在你執行 pnpm install 時會自動安裝所有專案的相依套件

內容要寫什麼呢?這邊先讓我們回頭看一下 Monorepo 的圖:

Mono Repository

我們可以看到一個 Git 底下會管理很多個專案,所以通常 Monorepo 的專案會長這樣:

1
2
3
4
5
- Git
- pnpm-workspace.yaml
- package.json
- apps
- packages

我們可以看到有一個 apps 資料夾,這個就是我們將會放置所有專案的地方,所以我們要在根目錄建立一個「apps」資料夾

1
mkdir apps

接著在「pnpm-workspace.yaml」檔案中寫入以下內容:

1
2
packages:
- apps/*

到這邊為止,已經將 Monorepo 的基本環境準備好了,接下來就是建立一個 Vue3 的專案,請你移動到「apps」資料夾中,並且建立一個 Vue3 的專案:

1
2
cd apps
pnpm create vue@latest

這邊我就示範建立一個「shopping」的專案

Vue Create

建立完成後,你專案應該會長這樣:

專案當前結構

接下來要請你打開 apps/shopping 的專案,並且找到 package.json 將 name 屬性改成 @apps/shopping,這樣才能讓 pnpm 知道這個專案是屬於 Monorepo 底下的專案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "@apps/shopping",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.2.29"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.9.2",
"vite": "^2.7.13"
}
}

Note
@app/專案名稱 這個是 Monorepo 的命名規則,專案名稱在 Monorepo 中是不能重複的,所以通常 Monorepo 底下的命名為「@」+「apps」+「/」+「專案名稱」,例如…我要建立一個 shopping 的專案,那麼專案名稱就會是「@apps/shopping」。

接下來請你回到最外層找到 package.json,並增加一個啟動指令在 scripts 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "monorepo-example-pnpm-vue3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"shopping": "pnpm --filter @apps/shopping"
},
"keywords": [],
"author": "",
"license": "ISC"
}

pnpm --filter @apps/shopping 這一段的指令意思是告訴 pnpm 這個專案是要執行 @apps/shopping 這個專案,這樣就可以讓我們在根目錄執行 pnpm shopping 時,就會啟動 @apps/shopping 這個專案。

接下來我們該如何啟動專案呢?只需要在根目錄執行以下指令即可:

1
pnpm shopping dev

Note
pnpm shopping dev 這個指令的意思是告訴 pnpm 這個專案是要執行 @apps/shopping 這個專案,並且執行 dev 這個指令,dev 指令則是 @apps/shopping 專案中的 scripts 中的 dev 指令。

建立公用依賴套件

接下來由於我們底下環境都是 Vue 相關的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "@apps/shopping",
// ...略過其他
"dependencies": {
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/test-utils": "^2.4.5",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"jsdom": "^24.0.0",
"prettier": "^3.2.5",
"vite": "^5.2.8",
"vite-plugin-vue-devtools": "^7.0.25",
"vitest": "^1.4.0"
}
}

所以我們就可以把 dependenciesdevDependencies 移動外層 package.json,作為一個共用,接下來只要將相關的指令加入到 scripts 即可,所以調整後會變成以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "monorepo-example-pnpm-vue3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev:shopping": "pnpm run -C apps/shopping dev",
"build:shopping": "pnpm run -C apps/shopping build",
"dev:order": "pnpm run -C apps/order dev",
"build:order": "pnpm run -C apps/order build"
},
// ...略過其他
}

Note
由於我們將 dependenciesdevDependencies 移動到根目錄,所以才把 --filter 改成了 -C,這樣才能讓 pnpm 知道這個專案是要執行哪個專案。

接下來輸入指令時,只需要輸入相對應的指令即可:

1
pnpm dev:shopping

專案啟動

那麼如果我們想要新增一個公用的套件,只需要在根目錄下安裝即可,例如…vue-loading-overlay 套件

1
pnpm add vue-loading-overlay -w

Note
在 Monorepo 中安裝套件時,需要加上 -w 參數,這樣才能讓 pnpm 知道這個套件是要安裝在所有專案中,如果是安裝 devDependencies 則需要加上 -D 參數,如:pnpm add eslint -w -D

建立公用的 Vue Component

假設今天我們有一個共用的 Button 元件,那我們在 Monorepo 該如何處理呢?首先請你在根目錄輸入以下指令

1
2
mkdir packages
cd packages

接著輸入以下指令一個新的 Vue3 專案

1
pnpm create vue@latest

接著把專案精簡成以下:

專案精簡

因為我們是要共用元件,並不需要 vite.config.js、public 以及 App.vue 這類檔案,接著打開 ui-lib 將 package.json 改成以下即可:

1
2
3
4
5
6
7
{
"name": "@packages/ui-lib",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "src/main.js",
}

Note
main 屬性非常重要,請務必指向到你的 main.js 檔案,這樣才能讓其他專案引入這個套件。

接下來該如何在 Shopping 使用呢?只需要輸入以下指令:

1
2
cd apps/shopping
pnpm add @packages/ui-lib

接著在 Shopping 的專案中使用這個元件即可:

1
2
3
4
5
6
7
<script setup>
import Button from '@packages/ui-lib';
</script>

<template>
<Button label="哈囉,我在 Shopping" />
</template>

這樣就可以在 Shopping 中使用 ui-lib 的 Button 元件了~

那麼以上就差不多是一個簡單的 Monorepo 環境,最後我也提供這個範例的 GitHub 連結給你參考。

Note
git clone 專案之後,只需要在根目錄輸入 pnpm install 即可安裝所有專案的相依套件。

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ