模組一|現況盤點與立案(Day 1–5)
昨天講的是看得見的重複:兩個 repo 裡 20 個一模一樣的檔案。今天講看不見的那種。
先給你一個謎題。假設你剛進這個專案,看到某個元件裡有這樣一行:
console.log(this.baseUrl)
你想知道 baseUrl 是什麼、從哪來。於是你在這個檔案裡搜 baseUrl。沒有。沒有 data、沒有 props、沒有 computed、沒有 import。
檔案裡完全找不到它的來源,但它就是能跑。
答案在專案入口檔案裡。我把它整理成三種型態,由輕到重。
import md5 from 'js-md5'
import { numberformat, downloadExcel } from './util/util'
Vue.prototype.$md5 = md5
Vue.prototype.numberformat = numberformat
Vue.prototype.downloadExcel = downloadExcel
Vue.prototype 是 Vue 2 提供的機制,意思是「把這個東西掛上去,之後所有元件都可以用 this.xxx 取用」。
好處很直觀:專案裡有幾百個地方要格式化數字,全部都寫一次 import 確實囉嗦。
這個做法本身不算錯,Vue 官方也支援。但它有個代價:this.numberformat 這行程式碼,沒有告訴你它從哪來。
這個就開始危險了:
import * as urls from '@/config/env'
// 把 env.js 匯出的所有東西,全部掛到 Vue 上
Object.keys(urls).forEach(key => {
Vue.prototype[key] = urls[key]
})
而那個設定檔匯出了五個東西:baseUrl、iconfontUrl、iconfontVersion、codeUrl、env。
所以謎題解開了:this.baseUrl 就是這樣冒出來的。沒有任何一行程式碼寫著「baseUrl 是元件的屬性」,它是被一個迴圈動態掛上去的。
這比型態一嚴重的地方在於:型態一至少你搜 Vue.prototype.numberformat 找得到那一行;型態二連搜都搜不到,因為 key 是變數。你只能反過來去讀那個設定檔,才知道到底掛了哪些東西上去。
這個是最重的:
Array.prototype.flattenDeep = function () {
const flat = arr =>
arr.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flat(val)) : acc.concat(val), [])
return flat(this)
}
Array.prototype 不是 Vue 的東西,是 JavaScript 語言本身的陣列。改它的意思是:這個網頁裡所有的陣列都多了一個 flattenDeep 方法,包括第三方套件裡的、瀏覽器 API 回傳的。
這種做法叫 monkey patching(猴子補丁)。它的危險在於:如果哪天官方或某個套件也加了同名方法,就會互相覆蓋,而出事時你完全不知道要往哪查。
一般文章接下來會開始講「全域污染有多糟」。但我想先問一個更基本的問題:
這些掛上去的東西,到底有沒有人在用?
這很好量。全域掛載的東西,在元件裡都是用 this.xxx 取用,所以數 this.xxx 出現幾次就好:
grep -rho "this\.numberformat" src --include='*.vue' | wc -l
我把全部 11 個掛上去的東西都數了一遍。結果是這樣:
| 掛上去的東西 | 型態 | 實際使用次數 |
|---|---|---|
numberformat |
工具函式 | 134 |
downloadExcel |
工具函式 | 28 |
$md5 |
工具函式 | 1 |
getSettlementCode |
工具函式 | 0 |
$showNextVersion |
開關旗標 | 0 |
baseUrl |
迴圈掛的設定 | 0 |
codeUrl |
迴圈掛的設定 | 0 |
iconfontUrl |
迴圈掛的設定 | 0 |
iconfontVersion |
迴圈掛的設定 | 0 |
env |
迴圈掛的設定 | 0 |
Array.prototype.flattenDeep |
改原生原型 | 0 |
11 個裡面,8 個完全沒人用。
其中最讓我印象深刻的是最後一行:為了讓所有陣列多一個方法,我們改動了 JavaScript 原生的 Array,然後全專案一次都沒用到。 那段程式碼唯一的出現位置,就是它自己的定義。
那個迴圈掛上去的五個設定變數也是一樣:this.baseUrl 確實每個元件都「能」用,但六年下來,一個元件都沒有真的用。專案裡真正需要它的兩個檔案,是規規矩矩用 import 拿的。

你可能會想:既然沒人用,刪掉不就好了?
問題是:你怎麼證明沒人用?
一般的程式碼有 import 關係。你想刪一個函式,IDE 可以告訴你「這個函式被三個地方引用」,刪掉沒被引用的東西是安全的。但全域的東西沒有 import 關係。
所以要證明 this.baseUrl 沒人用,你得:
this.baseUrl — 但如果有人寫成 this['baseUrl'] 呢?baseUrl — 會撈到一堆同名但無關的東西每一步都不難,但加起來就是「一個下午」,而刪掉它的好處是「少三行程式碼」,沒有人會做這筆交易,何況還要承擔萬一漏搜的風險。
於是它們就留下來了,一年又一年。這就是我在 Day 3 說的那句話的另一種形式:技術債不是讓你多寫程式碼,是讓你不敢刪程式碼。

如果全域掛載的東西全都沒人用,那反而好辦,整段刪掉就是了。
真正的麻煩是:numberformat 被用了 134 次。
這代表它是這個專案的基礎設施。它可能有處理千分位、小數點、負數、貨幣符號的邏輯,散落在 134 個地方依賴著。你不能刪它,你只能決定「重構之後,這個東西要以什麼形式存在」。
所以全域魔法真正的代價,不是「有人濫用」,是它讓必需品和垃圾長得一模一樣。兩者都是 this.xxx、都沒有來源、都不能靠工具分辨。你得一個一個去數,才知道哪些能刪、哪些要保留。
我盤到這裡才理解一件事:問題的根源在 API 設計,不在人的紀律。 Vue.prototype 這個機制本身就在鼓勵你把東西往上丟,一行程式碼、立刻生效、不需要任何解釋。沒有摩擦的事情,六年後一定會累積成一坨。
還有一個更隱蔽的,因為它不長得像全域魔法。
專案裡有一個「全域事件廣播站」(用 Node.js 的事件機制搬到瀏覽器裡,一邊 emit、另一邊 on 起來接)。它廣播的是「選單資料載入完成了」「權限資料載入完成了」這兩件事。
跟前面三種不同的是:這個是真的在用,而且在最核心的路徑上,路由怎麼產生、按鈕權限怎麼判斷,都靠它。
它的問題不是沒人用,是它讓因果關係變成隱形的。你看著路由設定檔,看不出來「路由是在使用者資料載入完成之後才產生的」,因為那個因果藏在一個字串裡。
而 Vue 3 把元件實例上的事件機制拿掉了,所以這種寫法在遷移時一定得處理。我們最後沒有去找替代套件,而是問了一個更根本的問題:這個事件到底在表達什麼? 完整的答案和做法在 Day 8。
我盤的當下腦袋裡沒有這些名詞,只覺得「這個怪怪的」。名字是後來補上的,但補上之後很好記。
YAGNI = You Aren't Gonna Need It,「你不會需要它」。
用搬家來理解最快:
你搬家的時候,一定有幾個箱子是「這個以後可能會用到」。
五年後你再搬一次家,那幾個箱子原封不動,連膠帶都沒拆。但你還是不敢丟,因為你不記得裡面裝什麼了,萬一有重要的東西呢?
於是它跟著你搬了第三次。
getSettlementCode、$showNextVersion、那五個設定變數、flattenDeep,它們被掛上去的時候,掛的人一定覺得「之後應該會用到」。六年後的答案是:沒有。
而 YAGNI 真正的成本不是「多寫了幾行」,是之後每一個看到它的人,都要花時間判斷它能不能丟。我今天花的這個下午,就是在替六年前那句「應該會用到」買單。
單一職責原則(SRP, Single Responsibility Principle)的一句話版本是:一個東西應該只有一個「被修改的理由」。
先看那個檔案在做幾件事:註冊 UI 元件庫、掛工具函式、改 JavaScript 原生陣列、載入字型、設定日期語系、註冊全域元件、掛環境變數。七件互不相干的事。
用一個比喻:
想像一把七合一的多功能工具:開瓶器、螺絲起子、剪刀、指甲刀、銼刀、開罐器、鑷子,全部長在同一個握柄上。
有天開瓶器的軸壞了。你要送修,於是螺絲起子、剪刀、指甲刀,全部一起消失兩個禮拜。
七件事共用一個檔案,就是七種工具共用一個握柄。要換字型,動它;要加一個工具函式,動它;要調日期格式,動它,而每次動它,都可能影響另外六件事。
這也是為什麼老專案的入口檔通常是最可怕的地方:它不是最複雜的檔案,但它是最多人有理由去改、而且改壞了整站陪葬的檔案。

左邊那些線是這篇的重點:每一條都代表「某個元件用了 this.xxx,但看不出來源」。線的數量不是問題,線沒有名字才是,你順著任何一條往回找,都會停在同一個看不出所以然的中心。右邊做的事只有一件:讓每一條線都變得可以追。
先講結果:大幅收斂,但沒有做到零全域。
import
那保留的兩個是什麼?其中一個是權限判斷函式,因為它會出現在幾百個地方,要求每個檔案都寫一次 import 是另一種折磨。
這是個明知故犯的取捨。 我們的理由是:只有兩個、有型別、而且寫進了專案守則。跟舊系統「掛了 11 個、8 個沒人用、沒有任何文件」不是同一件事。
一、那個「數用量」的方法有漏洞。 我數的是 this.xxx,但如果有人寫成 this['xxx']、或用變數動態取值,我就抓不到。所以「8 個沒人用」嚴格說是「8 個用我的方法找不到人在用」,這兩者不一樣。
實務上我的處理是:真的要刪之前,先把它改成會噴錯的版本放一個版本週期(例如讓它印出警告),確認沒有人踩到再刪。這比純靠 grep 安全,但要多等一輪。
二、我們保留的那兩個全域,可能會變成下一個六年的問題。 我在上面說「只有兩個、有型別、有文件」所以沒關係,但當年那個人掛第一個 Vue.prototype 的時候,大概也是這樣想的。
這條線是憑感覺畫的,而且沒有機制阻止它往上長。 我們沒有加任何檢查來防止有人再掛第三個、第四個。Day 18 講權限的時候會再回到這個取捨,包括我到現在也不完全確定它是對的。
盤點全域掛載的時候,我學到三件事:
明天 Day 5,模組一收尾:這些帳算完之後,我怎麼把它變成一份能拿到預算的提案。包含一個我一直被問的問題(「重構要多久?」),以及為什麼這個問題有兩個相差三倍的正確答案。