模組二|選型、語言遷移與地基(Day 6–11)
官方文件說主應用要放在 apps/ 底下。我們的主應用有 829 個檔案,全部待在根目錄的 src/。
這不是還沒做完,是刻意的。今天講為什麼。
先講結論:我們沒有從零搭,而是用了一套現成的開源 admin 框架(Vue 3 生態裡這類專案不少,挑法後面講)。
但這篇不是要推薦它。我想講的是選型當下真正在權衡什麼,以及一個更有意思的問題:選了之後,你必須全盤照收嗎?
第一個問題很基本:Vue 3 的生態這麼成熟,為什麼不自己組一套?
先把「自己組一套」實際要做的事列出來:
登入頁與 token 流程、整體版面(側邊選單 + 頂部列)、多頁籤、動態路由、權限判斷、主題切換、多語系、請求層封裝、環境變數與多環境建置、程式碼風格規則、提交訊息規範……
這串東西有一個共同點:它們跟你的業務價值一點關係都沒有。
沒有人會因為你的多頁籤實作得很優雅而多付一毛錢。但每一項你都得做,而且做不好會每天扎你。
所以選現成的後台底座,本質上是買時間。問題只在於:買哪一個、以及要接受它多少。

我推上去的這個齒,齒距剛好對得上;地上那個對不上的,我沒有再試第二次
我後來回頭整理,覺得選底座應該看三件事:
一、社群還活著嗎? 這個最容易查:最近一次更新是什麼時候、issue 有沒有人回、有沒有跟上框架的大版本。一個兩年沒更新的底座,等於你接手了一份別人的技術債。
二、我能只拿一部分嗎? 有些底座是「全有全無」的:你想用它的版面,就得連它的狀態管理、請求層、甚至它的目錄結構一起吞下去。可拆解度越高,你之後後悔的成本越低。
三、它的心智模型跟舊系統接得上嗎?
第三點最關鍵,而且最容易被忽略。這也是我們最後那個選擇的主因。
我們的舊系統是後端驅動的動態路由:使用者登入後,後端回傳一份選單清單,前端拿到之後才即時產生對應的路由。權限也是同一套資料決定的。
這個設計有它的道理(權限調整不用改前端、不用重新部署),而且六年下來,整個公司的權限管理流程都是繞著它建立的,後台有人在維護那份選單、有 SOP、有測試流程。
我們選的那套框架原生就支援「後端回傳選單、前端動態產生路由」,所以搬過去的時候,權限的心智模型完全不用改(實作細節怎麼搬,Day 17 會講)。
反過來想就知道這件事多重要:如果選了一個假設「路由寫死在前端」的底座,那要換的就不只是程式碼,是整個公司的權限管理流程,後台維護介面、SOP、測試腳本全部要重做。那已經遠遠超出前端專案的範圍了。
所以第三點的白話版是:選底座不是在選功能清單,是在選「你願不願意讓公司的既有流程遷就它」。
選定之後,還有第二個問題:要接受它多少?
它的新版官方推薦完整的 monorepo 架構。
先解釋一下 monorepo 是什麼:
假設你們公司有三個前端專案。
一般做法(多 repo):三個 repo,各自獨立。共用的工具函式?各自複製一份。
monorepo:一個 repo,裡面有三個資料夾放三個專案,再加一個資料夾放共用的東西。三個專案指向同一份共用程式碼,改一次三邊都生效。
官方的建議結構大概是這樣:各個應用放在 apps/ 底下、共用的能力層抽進 packages/。
而我們沒有這樣做。
我們的實際結構是:
專案根目錄/
├── src/ ← 主應用整包在這裡(829 個檔案)
├── apps/
│ ├── portal-view/ ← 幾乎是空的
│ └── test-server/ ← 一個小型測試伺服器
├── packages/ ← 2 個共用套件
├── internal/ ← 4 個建置與規範設定
└── vite.config.ts ← 打包的是根目錄的 src/
換句話說:monorepo 的殼留著,但主應用沒有搬進 apps/,還是待在根目錄的 src/。
數字很直白:根目錄 src/ 有 829 個檔案,而 apps/ + packages/ + internal/ 加起來只有 63 個。93% 的程式碼都在那個「不該在」的地方。

比例長條圖:根目錄 src/ 佔undefined個檔案共 93%,apps/ packages/ internal/ 合計undefined個佔 7%
右邊那一小條就是整個 monorepo 骨架實際裝的東西。我把這張圖放進來不是為了自嘲,是因為它精準地畫出了本篇的代價:目錄結構會對新人說謊。看到 apps/ 和 packages/ 的人,合理地會以為程式碼在那裡。
這件事我一開始覺得是「還沒做完」,後來才理解是刻意的。
從舊系統搬過來的是一個大型單體應用。如果在遷移的同時,還要把它拆成符合官方結構的多個 app,你會同時處理兩件事:
而這兩件事出錯的時候長得一模一樣:畫面壞掉、找不到模組。你會分不出來是搬邏輯搬錯了,還是路徑改錯了。
Day 5 講過我們刻意讓計畫「隨時可以停」。同樣的邏輯在這裡:在最不穩定的階段,不要增加額外的變因。
用整修餐廳的比喻繼續講:
你要換廚房設備(Vue 2 → Vue 3),這已經是大工程了。
這時候有人說:「順便把廚房移到二樓吧,那樣動線比較好。」動線確實比較好。但你會搞不清楚上菜慢是因為新爐具不熟,還是因為樓梯。
雖然沒搬主應用,但我們確實抽出了六個共用套件。它們有個明顯的共同特徵:
| 套件 | 內容 | 跟業務有關嗎 |
|---|---|---|
| 建置設定 | 打包設定、外掛、環境處理 | 無 |
| ESLint 設定 | 程式碼風格規則 | 無 |
| Stylelint 設定 | 樣式風格規則 | 無 |
| TypeScript 設定 | 型別編譯設定 | 無 |
| 共用 hooks | 請求快取、防抖、節流、重試、輪詢 | 無 |
| 共用型別 | 通用型別定義 | 無 |
六個全部跟業務無關。
這不是巧合。這正是 Day 3 那個發現的延伸:當時我比對兩個舊 repo,發現 20 個一字不差的檔案,而它們全部都是跟業務無關的基礎建設。
換句話說:六年的實際使用行為,早就把「哪些東西該共用」的答案標好了。我們不需要猜,只要照著抄。
這個決定的性質是:先拿走便宜的好處,貴的留到有把握再做。 抽建置設定的風險趨近於零;抽業務元件的風險極高(Day 29 會專門講為什麼)。

我把散了一地的東西掃攏了,結果還是兩堆,而且兩堆已經不太一樣
寫這篇的時候,我做了一件事:我去比對了兩個新專案的那六個共用套件。
結果是這樣:
| 套件 | 兩個專案的差異 |
|---|---|
| 共用 hooks | 0 行 |
| 共用型別 | 0 行 |
| TypeScript 設定 | 0 行 |
| ESLint 設定 | 5 行 |
| Stylelint 設定 | 5 行 |
| 建置設定 | 13 行 |
看出問題了嗎?
這六個套件,兩個專案各有一份。
monorepo 讓「同一個 repo 內」共用了,但兩個 repo 之間還是各自維護一份。而且三個已經開始出現差異了,就跟 Day 3 那個路由守衛一模一樣。
我當下的反應是:這不就是六年前那個問題嗎?
嚴格說起來,是的。只是規模不同:
| 舊系統 | 新系統 | |
|---|---|---|
| 兩邊同路徑的檔案 | 172 個 | 6 個套件 |
| 完全相同 | 20 個 | 3 個 |
| 已經開始長歪 | 33 個 | 3 個 |
從 172 個檔案縮到 6 個套件,這是巨大的進步。但性質沒有改變:它還是「兩份,而且正在變得不一樣」。
所以要精確地說:monorepo 解決的是「一個 repo 內的重複」,它完全沒有解決「跨 repo 的重複」。
用公寓來比喻的話:
舊系統像兩棟獨棟房子,各自有水塔、各自有電箱,什麼都要維護兩份。
新系統是蓋了兩棟公寓。每棟公寓裡的住戶共用電梯和水塔了,這確實省了很多事。
但兩棟公寓之間,還是各有一套水塔。
那為什麼不乾脆把兩個專案合成一個 repo?這個問題很好,而且我們到現在也沒做。理由、判斷準則、以及我認為的時機點,留到 Day 29。
不照抄官方架構,有兩個實際成本:
一、專案結構跟官方文件對不上。 新人照著官方文件找檔案會找不到:文件說在 apps/某個應用/src/router,我們的在根目錄 src/router。這代表你必須自己寫一份架構說明,而且要維護它。你等於放棄了「官方文件也是我們的文件」這個好處。
二、之後如果想搬回官方結構,成本只會更高。 我們每多寫一個月的程式碼,那個「之後再搬」的選項就更貴一點。老實說我不確定我們還會不會搬,「暫時這樣」在軟體專案裡通常等於「永遠這樣」,這點要有心理準備。
明天 Day 7 進入這個模組真正的重頭戲:Vue 2 到 Vue 3,this 消失之後。我會講為什麼我們沒有用自動轉換工具,以及那個檔名拼錯了六年、沒人敢改的 mixin。