iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Modern Web

一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年系列 第 9

Day 9|換掉 UI 元件庫:8,023 個標籤,和一個我沒預料到的結果

  • 分享至 

  • xImage
  •  

模組二|選型、語言遷移與地基(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 次

比舊的還多。

我看到這個數字的時候愣了一下,然後去翻了幾頁的原始碼,確認不是統計錯誤:新系統的搜尋表單,確實還是一個一個手寫的。

為什麼表格收斂了、表單沒有?

因為表格欄位的變化維度是有限且可窮舉的:標題、寬度、對齊、格式化、要不要排序。就這幾種,所以能變成資料。

而業務表單不是。一個真實的後台搜尋表單會有這些東西:

  • 選了 A 之後,B 的選項要重新去後端拿
  • 只有當使用者是某種角色時,才顯示某個欄位
  • 日期區間不能超過三個月,但如果勾了「匯出」就放寬到一年
  • 這個欄位在總控端是必填,在另一個系統是選填

這些規則你可以硬塞進設定檔,但塞完之後,那個設定檔會長出自己的一套語法,比手寫還難讀,而且只有你看得懂。

所以我們選擇了另一條路,這也回答了「那你們到底抽了什麼」。

更誠實的版本:框架給了方案,我們沒用

那盒整組的我一次都沒拆,真正在幹活的是手上這把被磨到發亮的小刀

寫這篇的時候我本來要說「我們自己抽了一批欄位元件」。然後我去數了一下,發現事情不是這樣。

我們用的那套 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 會論證),但我也必須承認:它可能只是「還沒想到更好的做法」的體面說法。

帶走什麼

  1. 換 UI 元件庫最貴的不是元件語法,是那些寫在模板裡、沒有名字的業務規則。 那 2,219 行的參數頁,價值不在程式碼,在那 24 個 v-if 記錄的規則。
  2. 「從模板搬到資料」比「換標籤名」重要得多。 前者讓你可以過濾、排序、共用;後者只是換了一件衣服。
  3. 抽象要抽在「變化維度可窮舉」的地方。 表格欄位可以,業務表單不行。
  4. 分清楚「原始碼」和「工具產物」。 後者不能手改,而且要寫進文件,因為它看起來跟原始碼一模一樣。
  5. 永遠不要拿「顯示給使用者看的文字」當判斷條件。 文案是會改的,而且改文案的人不會知道你在拿它做比對。

明天 Day 10 講一個我們刻意打折的決定:導入 TypeScript,但把最嚴格的檢查關掉了。這是妥協還是策略?我會給出我們當時的理由,以及兩年後回頭看,這個決定的代價出現在哪裡。


上一篇
Day 8|Vuex 裡有六個從沒打開過的空模組
下一篇
Day 10|`"strict": false, // 啟用嚴格模式`
系列文
一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言