模組二|選型、語言遷移與地基(Day 6–11)
這個專案是一個活了六年的 Vue 2 後台,我花了兩年把它升到 Vue 3。升級的時候 UI 元件庫也得換,因為 Vue 2 時代的那套沒有 Vue 3 版本。
這件事聽起來是整個遷移裡最機械的部分:查對照表、改標籤名、調 props。網路上甚至有人做過對照表。
先看一下要改的量。我把舊專案裡用到的元件標籤數出來:
| 標籤 | 出現次數 | 分布檔案 |
|---|---|---|
表格欄位 el-table-column |
3,354 | 166 |
表單項 el-form-item |
2,435 | 171 |
輸入框 el-input |
970 | 164 |
下拉選單 el-select |
714 | 120 |
彈窗 el-dialog |
383 | 105 |
日期選擇 el-date-picker |
167 | 85 |
| 合計 | 8,023 |
八千多個標籤。 這已經不是「查對照表」的規模了。
我一開始的直覺是寫個腳本批次取代:el-table → 新的表格標籤、el-form-item → 新的表單項標籤,跑完再手動修殘局。
這個想法在第一個頁面就死了。
原因不是 API 名稱不同,是兩套元件庫對同一件事的假設不同。舉三個實際遇到的:
一、表格欄位的宣告方式,根本不是同一種東西。
舊的寫法是用子標籤宣告欄位:
<el-table :data="list">
<el-table-column prop="orderNo" label="訂單編號" width="180" />
<el-table-column prop="amount" label="金額" align="right" />
<el-table-column prop="status" label="狀態">
<template slot-scope="scope">
<span>{{ formatStatus(scope.row.status) }}</span>
</template>
</el-table-column>
</el-table>
新的寫法是用一個陣列描述欄位:
<a-table :columns="columns" :data-source="list" />
const columns = [
{ title: '訂單編號', dataIndex: 'orderNo', width: 180 },
{ title: '金額', dataIndex: 'amount', align: 'right' },
{ title: '狀態', dataIndex: 'status' },
]
這不是換個標籤名就好,是「欄位定義從模板搬到資料」。你沒辦法用字串取代完成這件事。
二、表單驗證的觸發時機不一樣。 兩邊都有驗證規則,但「什麼時候驗、驗完錯誤訊息放哪、整份表單怎麼一次驗完」的預設行為不同。照抄過去的結果是驗證看起來有跑,但某些邊界情況會靜靜地放行。
三、彈窗的生命週期不一樣。 「關閉時要不要銷毀內容」「開啟時內部元件會不會重新初始化」,兩邊預設值不同。這直接影響「關掉再打開,上次填的東西還在不在」,而這種 bug 測試的時候很難想到要試。
所以我們的做法是:不翻譯,重寫。 每一頁搬過去的時候,順便重新想一次這一頁的互動該長什麼樣。
反正都要一頁一頁看過(Day 7 講過我們也沒用自動轉換工具),那不如在同一趟裡把六年前的權宜之計一起處理掉。

回頭看,換元件庫最大的收穫不是 UI 變好看,是上面第一點那個差異。
舊的寫法裡,一個表格有幾個欄位、每個欄位多寬、怎麼對齊,這些資訊是「畫」在模板裡的。想知道這頁有哪些欄位,你得讀模板;想根據權限動態隱藏某個欄位,你得在模板裡加 v-if。
新的寫法裡,欄位定義是一個普通的陣列。這代表:
// 想根據權限過濾欄位?普通的陣列操作而已
const columns = allColumns.filter(col => hasPermission(col.auth))
// 想把排序邏輯抽出來共用?也只是個函式
const columns = withSorter(baseColumns, ['amount', 'createdAt'])
我們後來就抽了一個處理「欄位排序」的共用函式,因為每個表格都要做這件事,而且每個人做法不一樣。抽出來之後,排序邏輯只有一份。
這件事有個名字:工廠模式(Factory Pattern)。
想像你要做西裝。
手工訂製:每一套從量身、裁布、縫製全部重來。每套都獨一無二,但你也沒辦法保證兩套的口袋位置一樣高。
工廠生產:你給尺寸,工廠照同一套流程出貨。少了點個性,但你確定每一套的口袋都在同一個位置。
在 admin 系統裡,你要的是後者。沒有人希望自己家的十個報表頁,欄位對齊方式有十種。
那 3,354 個「畫在模板裡」的欄位標籤,最後變成了一批可以被過濾、被排序、被共用的資料。這是換元件庫真正付了錢買到的東西。
我原本以為表單也會有類似的收穫。所以我去數了新專案的表單項標籤。
結果:
| 舊系統 | 新系統 | |
|---|---|---|
| 表單項標籤 | 2,435 次 | 2,842 次 |
比舊的還多。
我看到這個數字的時候愣了一下,然後去翻了幾頁的原始碼,確認不是統計錯誤:新系統的搜尋表單,確實還是一個一個手寫的。
為什麼表格收斂了、表單沒有?
因為表格欄位的變化維度是有限且可窮舉的:標題、寬度、對齊、格式化、要不要排序。就這幾種,所以能變成資料。
而業務表單不是。一個真實的後台搜尋表單會有這些東西:
這些規則你可以硬塞進設定檔,但塞完之後,那個設定檔會長出自己的一套語法,比手寫還難讀,而且只有你看得懂。
所以我們選擇了另一條路,這也回答了「那你們到底抽了什麼」。

寫這篇的時候我本來要說「我們自己抽了一批欄位元件」。然後我去數了一下,發現事情不是這樣。
我們用的那套 admin 框架,本身就附了一整套 schema 化的表格與表單機制:你寫一個設定陣列,它幫你生出整個表格或表單,連「會自己去打 API 拿選項的下拉選單」這種元件都準備好了。
我把它的規模數出來:
| 檔案數 | 行數 | 業務頁面實際使用 | |
|---|---|---|---|
| 框架附的表格機制 | 43 | 5,832 | 1 頁(而且是框架自帶的錯誤紀錄頁) |
| 框架附的表單機制 | 26 | 3,721 | 幾乎為 0 |
九千多行的 schema 化機制,我們的業務頁面一頁都沒用。
那業務頁面在用什麼?
| 實際用法 | 涵蓋頁面 |
|---|---|
| 直接寫表格標籤 | 162 頁 |
| 直接寫表單項標籤 | 148 頁 |
| 一個團隊自己寫的排序 hook | 25 頁 |
最後那一行是重點。團隊唯一真正推廣開來的抽象,是一個 457 行的 hook,它只做一件事:把表格欄位的排序邏輯集中管理。
比例是這樣的:框架給的 9,553 行,用了 0;自己寫的 457 行,用了 25 個頁面。
我的理解是:框架的 schema 化方案要求你先接受它的世界觀。 你得學它的設定語法、照它的方式描述欄位、遇到它沒設計到的情況時再想辦法繞。
而那個 457 行的 hook 不要求你改變任何習慣:表格照你原本的方式寫,只是排序那段呼叫它。它的採用成本趨近於零,所以它活了下來。
這件事我覺得比「我們抽了一批元件」誠實,也有用得多:
一個抽象會不會被採用,跟它多完整無關,跟它要求使用者改變多少有關。
完整的理由、以及我對「全設定化」的看法,Day 21 會專門講。今天先給結論:
抽象要抽在「變化維度可窮舉」的地方。抽在維度無限的地方,你只是把複雜度搬了個位置,還順便發明了一種新語法。
換元件庫的時候我們啟用了「自動引入」:不用在每個檔案寫 import,工具會掃描你的模板、自動註冊用到的元件。
它會產生一個型別宣告檔,目前註冊了 388 個元件。
而這個檔案是工具自動產生的,不能手改。
聽起來很basic,但這件事一定會有人踩:某天有人發現這個檔案裡少了一個元件,或是型別不太對,於是手動改了它、提交了。下一次有人跑建置,檔案被重新產生,改動消失。然後他會再改一次。
我們的處理方式是把它寫進專案守則的「地雷」章節,不是靠口頭提醒,是寫成文件。
這一類「工具產物」在現代前端專案裡越來越多(自動產生的型別、路由、API client)。它們的共同特徵是:看起來像原始碼,但其實是編譯結果。分不清楚這件事,就會浪費很多次無效的修改。
因為決定「重寫而不是翻譯」,我們在搬的過程中也順手處理掉一些六年前的權宜之計。
一個代表性的例子:舊專案有個參數設定頁,2,219 行,裡面有 24 個 v-if、30 個 v-else、14 個提示框,還有 88 次翻譯函式呼叫。
它在做什麼?用一長串條件判斷,決定當前這個參數該顯示哪一段說明文字。
<!-- 大致的結構長這樣 -->
<span v-if="paramName === $t('...A')">說明 A</span>
<span v-else-if="paramName === $t('...B')">說明 B</span>
<span v-else-if="paramName === $t('...C')">說明 C</span>
<!-- ...再來十幾個 -->
注意最糟的地方:它拿「翻譯後的文字」去做比對。也就是說,如果哪天有人改了中文文案,這串判斷會全部失效,而且不會報錯,只是說明文字集體消失。
重寫的時候,這段變成一張對照表:
const PARAM_HINTS = {
maxAmount: '單筆上限說明…',
dailyLimit: '每日限額說明…',
}
模板只剩一行。判斷條件從「顯示出來的文字」換回「資料的 key」,改文案再也不會弄壞邏輯。
這種修正沒辦法靠自動翻譯工具做到,只能在一頁一頁重寫的時候被人看見。
一、換元件庫的工作量會被嚴重低估。 因為它看起來像機械勞動,實際上每一頁都要做設計決策。我建議估工時的時候,把「查對照表的時間」乘以三。
二、視覺上會有一段不一致期。 新舊兩套元件庫的間距、圓角、預設色系都不同。遷移期間某些頁面新、某些頁面舊,使用者會抱怨「怎麼看起來不一樣」,這個要事先跟業務講,不然會被當成 bug 回報。
三、表單的部分我們並沒有變好。 標籤數甚至更多了。我可以說這是刻意的取捨(Day 21 會論證),但我也必須承認:它可能只是「還沒想到更好的做法」的體面說法。
v-if 記錄的規則。明天 Day 10 講一個我們刻意打折的決定:導入 TypeScript,但把最嚴格的檢查關掉了。這是妥協還是策略?我會給出我們當時的理由,以及兩年後回頭看,這個決定的代價出現在哪裡。