iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Modern Web

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

Day 4|`this.baseUrl` 從哪來?全域魔法與那些沒人敢刪的東西

  • 分享至 

  • xImage
  •  

模組一|現況盤點與立案(Day 1–5)

昨天講的是看得見的重複:兩個 repo 裡 20 個一模一樣的檔案。今天講看不見的那種。

先給你一個謎題。假設你剛進這個專案,看到某個元件裡有這樣一行:

console.log(this.baseUrl)

你想知道 baseUrl 是什麼、從哪來。於是你在這個檔案裡搜 baseUrl。沒有。沒有 data、沒有 props、沒有 computed、沒有 import

檔案裡完全找不到它的來源,但它就是能跑。

三種全域魔法

答案在專案入口檔案裡。我把它整理成三種型態,由輕到重。

型態一:把工具函式掛到 Vue 上

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]
})

而那個設定檔匯出了五個東西:baseUrliconfontUrliconfontVersioncodeUrlenv

所以謎題解開了:this.baseUrl 就是這樣冒出來的。沒有任何一行程式碼寫著「baseUrl 是元件的屬性」,它是被一個迴圈動態掛上去的。

這比型態一嚴重的地方在於:型態一至少你搜 Vue.prototype.numberformat 找得到那一行;型態二連搜都搜不到,因為 key 是變數。你只能反過來去讀那個設定檔,才知道到底掛了哪些東西上去。

型態三:改動 JavaScript 原生的東西

這個是最重的:

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 拿的。

那為什麼不刪?

https://ithelp.ithome.com.tw/upload/images/20260809/201834798PIpZ8FGpE.png

你可能會想:既然沒人用,刪掉不就好了?

問題是:你怎麼證明沒人用?

一般的程式碼有 import 關係。你想刪一個函式,IDE 可以告訴你「這個函式被三個地方引用」,刪掉沒被引用的東西是安全的。但全域的東西沒有 import 關係。

所以要證明 this.baseUrl 沒人用,你得:

  1. 全專案搜 this.baseUrl — 但如果有人寫成 this['baseUrl'] 呢?
  2. baseUrl — 會撈到一堆同名但無關的東西
  3. 而且你還要對第二套系統再做一次(記得昨天講的姊妹專案嗎)

每一步都不難,但加起來就是「一個下午」,而刪掉它的好處是「少三行程式碼」,沒有人會做這筆交易,何況還要承擔萬一漏搜的風險。

於是它們就留下來了,一年又一年。這就是我在 Day 3 說的那句話的另一種形式:技術債不是讓你多寫程式碼,是讓你不敢刪程式碼。

最難的是好壞混在一起

https://ithelp.ithome.com.tw/upload/images/20260809/20183479BDgtviW1zp.png
如果全域掛載的東西全都沒人用,那反而好辦,整段刪掉就是了。

真正的麻煩是:numberformat 被用了 134 次。

這代表它是這個專案的基礎設施。它可能有處理千分位、小數點、負數、貨幣符號的邏輯,散落在 134 個地方依賴著。你不能刪它,你只能決定「重構之後,這個東西要以什麼形式存在」。

所以全域魔法真正的代價,不是「有人濫用」,是它讓必需品和垃圾長得一模一樣。兩者都是 this.xxx、都沒有來源、都不能靠工具分辨。你得一個一個去數,才知道哪些能刪、哪些要保留。

我盤到這裡才理解一件事:問題的根源在 API 設計,不在人的紀律Vue.prototype 這個機制本身就在鼓勵你把東西往上丟,一行程式碼、立刻生效、不需要任何解釋。沒有摩擦的事情,六年後一定會累積成一坨。

第四種:一個假裝不是全域的全域

還有一個更隱蔽的,因為它不長得像全域魔法。

專案裡有一個「全域事件廣播站」(用 Node.js 的事件機制搬到瀏覽器裡,一邊 emit、另一邊 on 起來接)。它廣播的是「選單資料載入完成了」「權限資料載入完成了」這兩件事。

跟前面三種不同的是:這個是真的在用,而且在最核心的路徑上,路由怎麼產生、按鈕權限怎麼判斷,都靠它。

它的問題不是沒人用,是它讓因果關係變成隱形的。你看著路由設定檔,看不出來「路由是在使用者資料載入完成之後才產生的」,因為那個因果藏在一個字串裡。

而 Vue 3 把元件實例上的事件機制拿掉了,所以這種寫法在遷移時一定得處理。我們最後沒有去找替代套件,而是問了一個更根本的問題:這個事件到底在表達什麼? 完整的答案和做法在 Day 8。

這幾件事都有名字

我盤的當下腦袋裡沒有這些名詞,只覺得「這個怪怪的」。名字是後來補上的,但補上之後很好記。

一、那 8 個沒人用的東西:YAGNI

YAGNI = You Aren't Gonna Need It,「你不會需要它」。

用搬家來理解最快:

你搬家的時候,一定有幾個箱子是「這個以後可能會用到」。

五年後你再搬一次家,那幾個箱子原封不動,連膠帶都沒拆。但你還是不敢丟,因為你不記得裡面裝什麼了,萬一有重要的東西呢?

於是它跟著你搬了第三次。

getSettlementCode$showNextVersion、那五個設定變數、flattenDeep,它們被掛上去的時候,掛的人一定覺得「之後應該會用到」。六年後的答案是:沒有。

而 YAGNI 真正的成本不是「多寫了幾行」,是之後每一個看到它的人,都要花時間判斷它能不能丟。我今天花的這個下午,就是在替六年前那句「應該會用到」買單。

二、那個入口檔:單一職責原則

單一職責原則(SRP, Single Responsibility Principle)的一句話版本是:一個東西應該只有一個「被修改的理由」。

先看那個檔案在做幾件事:註冊 UI 元件庫、掛工具函式、改 JavaScript 原生陣列、載入字型、設定日期語系、註冊全域元件、掛環境變數。七件互不相干的事。

用一個比喻:

想像一把七合一的多功能工具:開瓶器、螺絲起子、剪刀、指甲刀、銼刀、開罐器、鑷子,全部長在同一個握柄上。

有天開瓶器的軸壞了。你要送修,於是螺絲起子、剪刀、指甲刀,全部一起消失兩個禮拜。

七件事共用一個檔案,就是七種工具共用一個握柄。要換字型,動它;要加一個工具函式,動它;要調日期格式,動它,而每次動它,都可能影響另外六件事。

這也是為什麼老專案的入口檔通常是最可怕的地方:它不是最複雜的檔案,但它是最多人有理由去改、而且改壞了整站陪葬的檔案。

新系統怎麼處理

https://ithelp.ithome.com.tw/upload/images/20260809/201834793sIaeNoS7a.png
左邊那些線是這篇的重點:每一條都代表「某個元件用了 this.xxx,但看不出來源」。線的數量不是問題,線沒有名字才是,你順著任何一條往回找,都會停在同一個看不出所以然的中心。右邊做的事只有一件:讓每一條線都變得可以追。

先講結果:大幅收斂,但沒有做到零全域。

  • 那個「用迴圈把設定整包掛上去」的做法:沒有了,改回老老實實 import
  • 改動原生陣列:沒有了
  • 掛在全域的工具:從 11 個減到 2 個

那保留的兩個是什麼?其中一個是權限判斷函式,因為它會出現在幾百個地方,要求每個檔案都寫一次 import 是另一種折磨。

這是個明知故犯的取捨。 我們的理由是:只有兩個、有型別、而且寫進了專案守則。跟舊系統「掛了 11 個、8 個沒人用、沒有任何文件」不是同一件事。

代價

一、那個「數用量」的方法有漏洞。 我數的是 this.xxx,但如果有人寫成 this['xxx']、或用變數動態取值,我就抓不到。所以「8 個沒人用」嚴格說是「8 個用我的方法找不到人在用」,這兩者不一樣。

實務上我的處理是:真的要刪之前,先把它改成會噴錯的版本放一個版本週期(例如讓它印出警告),確認沒有人踩到再刪。這比純靠 grep 安全,但要多等一輪。

二、我們保留的那兩個全域,可能會變成下一個六年的問題。 我在上面說「只有兩個、有型別、有文件」所以沒關係,但當年那個人掛第一個 Vue.prototype 的時候,大概也是這樣想的。

這條線是憑感覺畫的,而且沒有機制阻止它往上長。 我們沒有加任何檢查來防止有人再掛第三個、第四個。Day 18 講權限的時候會再回到這個取捨,包括我到現在也不完全確定它是對的。

今天的心法

盤點全域掛載的時候,我學到三件事:

  1. 先數用量,再罵設計。 「全域污染很糟」是正確但無用的評論;「11 個裡有 8 個沒人用」才能拿去開會。
  2. 難刪的東西不是最複雜的,是沒有引用關係的。 判斷技術債輕重的標準只有一個:你要花多少力氣,才能證明它可以被移除。
  3. 沒有摩擦的捷徑,一定會被累積。 這不是紀律問題,是設計問題,因為你不能期待六年裡的每一個人都保持克制。

明天 Day 5,模組一收尾:這些帳算完之後,我怎麼把它變成一份能拿到預算的提案。包含一個我一直被問的問題(「重構要多久?」),以及為什麼這個問題有兩個相差三倍的正確答案。


上一篇
Day 3|兩套系統,20 個一模一樣的檔案,和幾個正在長歪的
下一篇
Day 5|「重構要多久?」這個問題有兩個相差三倍的答案
系列文
一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言