模組四|核心系統改寫(Day 17–21)
這篇的主軸,是被我自己的實測推翻的。
我原本要寫的是「權限從路由級長到按鈕級」:舊系統只能控制「你能不能進這一頁」,新系統才做到「你能進來看,但不能匯出」。
然後我去數了一下,發現完全不是這樣。
舊系統早就有按鈕級權限了。而且它有兩套。
| 舊系統的按鈕級權限 | 使用次數 | 分布檔案 |
|---|---|---|
| 一個自訂指令 | 101 | 42 |
| 一個掛在全域的判斷函式 | 557 | 133 |
| 兩套都用的檔案 | — | 20 |
所以真正的問題不是「有沒有」,是「為什麼有兩個」。
寫法 A:自訂指令
<a-button v-permission:edit-disabled="'/order/export'">匯出</a-button>
冒號後面那個參數決定「沒權限的時候要怎麼表現」:是隱藏、還是變成不可點擊。
寫法 B:全域判斷函式
<a-button v-if="$hasPermission('/order/export')">匯出</a-button>
自己用 v-if 決定顯示邏輯。
兩者的權限碼都是 API 路徑字串,這點是一致的。
一開始我以為這是兩套獨立實作,後來讀了原始碼才發現——它們定義在同一個檔案裡,而且呼叫的是同一個判斷函式。
指令的內部長這樣:
// 指令被掛上元素時
const hp = hasPermission(permission)(value) // ← 同一個函式
if (!hp) { /* 隱藏或禁用 */ }
而那個全域函式就是:
Vue.prototype.$hasPermission = hasPermission(permission) // ← 同一個函式
所以:這不是兩套邏輯,是同一套邏輯的兩個入口。
判斷結果永遠一致,不會有「指令說可以、函式說不行」的矛盾。從正確性來說,它沒有 bug。
那問題在哪?

問題在於每個要用它的人,都得先做一個不必要的決定。
你要在一個按鈕上加權限判斷。
你打開旁邊的檔案參考,看到用的是指令。
再打開另一個檔案,看到用的是函式。哪一個才是這個專案的慣例?
沒有人知道。因為兩個都有人用,而且用量都不小。
於是每個人的做法是:照抄離自己最近的那個檔案。
這解釋了為什麼會有 20 個檔案兩套都用,那不是誰的錯,是不同時期複製了不同來源的結果。
我覺得這件事有個更普遍的形式:
當一個團隊裡「同一件事有兩種正確做法」,真正的成本不是選錯,是每個人都要重新選一次。
而且這個成本是複利的:兩套並存越久,兩邊的用量都越大,之後想統一的代價就越高。舊系統的 101 對 557,就是六年複利的結果。
讀那個檔案的時候,我還發現兩件更值得講的事。
if (process.env.APP_PERMISSION !== 'on') {
// 判斷函式永遠回傳 true
$hasPermission = () => true
// 指令變成什麼都不做
directive('permission', { inserted() {} })
}
這顯然是為了本機開發方便,不用真的登入拿權限,畫面上所有按鈕都看得到。
我理解這個需求。但它的意思是:整個系統的按鈕權限,取決於一個環境變數。
如果那個變數在某個環境設錯了,所有按鈕會全部開放,而且畫面上完全看不出異常,因為「按鈕都看得到」正是它該有的樣子。
這種開關的危險之處在於:它失效的方式是「看起來很正常」。
如果要保留這種便利,我認為至少要做兩件事:在非開發環境啟用時要噴出明顯的警告,以及讓建置流程直接擋掉錯誤的組合。「靠設對」是不夠的。

// 權限資料載入完成的時候,重新綁一次
userEvent.on('permissionLoad', data => {
permission = data
$hasPermission = hasPermission(permission) // ← 整個換掉
})
$hasPermission 這個全域函式,在執行期會被重新賦值。
也就是說:同一個 $hasPermission('/order/export') 呼叫,在權限載入前後行為不同。載入前權限資料是空的,函式直接回傳 false。
這在多數情況下沒問題(畫面通常在權限載入後才渲染),但它代表這個函式的行為跟時間有關——而這正是最難除錯的那類問題。
順帶一提,那個 permissionLoad 就是 Day 4 講的「假的 EventBus」。三篇文章講的三個問題,最後都指向同一個檔案。
新系統只留一個入口:一個判斷函式,864 次、分布 169 個檔案。
另外兩種替代方案幾乎沒有人用:一個指令用了 1 次、一個包裝元件用了 3 次。這代表收斂是真的成功,不是「訂了規範但大家各寫各的」。
而 864 次這個數字,順便回答了 Day 4 留下的問題。
Day 4 我批評舊系統把一堆東西掛在全域,然後坦承新系統還是保留了兩個全域掛載,其中一個就是這個權限判斷函式。
當時我說「這條線是憑感覺畫的」。現在有數字了:169 個檔案。
要求 169 個檔案各自寫一行 import,是另一種折磨,而且它會讓「加權限判斷」這件事多一道摩擦,而摩擦會讓人省略它。權限這種東西,你不會希望它變得比較麻煩。
所以我現在對這個取捨的看法是:它是對的,但理由不是「方便」,是「我們希望這件事沒有阻力」。
這跟舊系統掛那 11 個全域的差別在於:
但我還是要說:我們沒有加任何機制阻止它長到第三個、第四個。 這條線目前只靠人的自覺守著。
一、收斂到一個入口,代表失去了指令那種「自動處理」的便利。 舊系統的指令可以指定「沒權限時要隱藏還是禁用」,那個行為是寫在指令參數裡的。改成純函式之後,每個地方都要自己寫 v-if 或 :disabled,寫法統一了,但重複的判斷變多了。
二、權限碼用 API 路徑字串,是一個我們繼承下來、沒有改的設計。 它的好處是不用另外維護一套權限碼;壞處是後端改路徑就會弄壞前端權限。我們沒動它,因為它同樣長進了組織流程(跟 Day 17 一樣的理由)。
三、統一是一次性的,維持統一不是。 目前 864 比 1 比 3 很漂亮,但沒有 lint 規則擋著,這跟 Day 15 的命名規範是同一個問題:文件擋不住下一個人。
明天 Day 19 講請求層。舊系統把 token、語系、錯誤處理、載入狀態、甚至「要打哪一套後端」全部塞進一個 182 行的檔案、兩個攔截器、16 個 if 分支裡。新系統拆成七個檔案——而我會講為什麼「可以關掉」比「功能完整」更重要。