模組二|選型、語言遷移與地基(Day 6–11)
舊專案裡有一個 14 行的檔案,我第一次打開它的時候看了三分鐘,然後決定把它寫成一整篇。
它同時示範了三個問題,而這三個問題正好解釋了「為什麼 Vue 3 要把 this 拿掉」。
先鋪一點背景。從 Vue 2 搬到 Vue 3,表面上是換寫法:data() 變成 ref()、methods 變成一般函式、this.xxx 變成 xxx.value。網路上一堆對照表,看起來像查字典就能做完。
但實際搬過之後我的感覺是:真正改變的不是語法,是「一個元件需要什麼」這件事被記錄在哪裡。
this 是什麼Vue 2 的元件長這樣:
export default {
data() {
return { count: 0 }
},
methods: {
add() {
this.count++ // this 拿得到 data
this.formatNumber() // this 也拿得到別的 method
},
},
}
這裡的 this 是一個集散地。Vue 在背後把很多東西掛上去:
data
props
methods、computed
Vue.prototype)好處是很方便,全部 this. 就有了。
代價是:當你在一個元件裡看到 this.someThing,你沒辦法只看這個檔案就知道它從哪來。
Day 4 講的全域魔法之所以會長成那樣,根源就在這裡:這個 API 設計讓「往 this 上丟東西」變成阻力最小的路徑。
理論講完,來看真的東西。
舊專案裡有一個 mixin 檔案,總共 14 行。我把它完整貼出來(變數名沒改,這就是原文):
export default {
methods: {
init_form_base_with_module(name, module) {
this[name] = null
this[name] = this.deep_clone(module)
},
abort_dialog_form() {
// this.close_dialog_base(name)
},
submit_dialog_form() {
// this.close_dialog_base(name)
},
},
}
14 行,三個問題。
這個檔案叫 formminxin.js。
正確拼法是 mi-x-in,這裡打成了 mi-nx-in。
它已經這樣存在六年了。沒有人改,因為改了要動 import 路徑,而你不確定還有誰在引用它。一個純粹的打字錯誤,因為沒有工具能保證安全改名,就變成了永久的。

abort_dialog_form 和 submit_dialog_form 這兩個方法,函式主體整個被註解掉了。
它們現在是兩個什麼都不做的空函式。
但它們還在。而且因為 mixin 的特性,任何用了這個 mixin 的元件,都「擁有」這兩個什麼都不做的方法。如果有元件在呼叫 this.submit_dialog_form(),它會安靜地什麼都不發生,不報錯,只是沒反應。
this.deep_clone 從哪來?看第一個方法:
init_form_base_with_module(name, module) {
this[name] = null
this[name] = this.deep_clone(module) // ← deep_clone 是誰?
}
deep_clone 不在這個檔案裡。這個檔案也沒有 import 任何東西。
我搜了一下,答案是:它在另一個 mixin 檔案裡(commontoolmixin.js 的第 49 行)。
也就是說,這個 mixin 依賴另一個 mixin,但這個依賴關係沒有寫在任何地方。它能跑,純粹是因為有個檔案剛好把兩個一起匯出:
import commontoolmixin from '@/mixins/module/commontoolmixin'
import formminxin from '@/mixins/module/formminxin.js'
export default [commontoolmixin, formminxin] // 剛好一起
如果有人只引入其中一個,它就會在使用者按下按鈕的那一刻爆掉,錯誤訊息是 this.deep_clone is not a function。
而且,編譯不會報錯、ESLint 不會報錯、IDE 不會標紅。這個地雷會一路走到正式環境。
那 14 行剛好把 mixin 的三個問題全示範了一遍:
一、來源不明。 元件裡出現一個沒定義的方法,你要翻遍所有 mixin 才找得到。
二、命名衝突會無聲覆蓋。 兩個 mixin 都有同名方法時,後面的贏,沒有任何警告。順帶一提,舊專案裡有兩個檔名都叫 numberMixin.js 的檔案(一個 84 行、一個 200 行),分別放在兩代統計模組底下。它們目前沒有被同一個元件引入,所以還沒出事,但這種安全是靠運氣的。
三、不敢刪。 就像那個拼錯的檔名。你不知道誰在用,所以只能留著。

Vue 3 的做法是把邏輯抽成一般函式(習慣上叫 composable,命名以 use 開頭):
// useFormBase.js
import { deepClone } from '@/utils/clone' // ← 依賴寫出來了
export function useFormBase() {
function initFormBase(module) {
return deepClone(module)
}
return { initFormBase }
}
元件裡這樣用:
const { initFormBase } = useFormBase()
差別不在語法,在這一行:
const { initFormBase } = useFormBase()
這一行本身就是文件。 它明白地說了:這個元件用了 useFormBase,而且只拿了 initFormBase 這個東西。
三個致命傷同時被解掉:
import,IDE 點一下就跳過去我後來才知道,這個轉變有個現成的說法叫 組合優於繼承(Composition over Inheritance)。
用一個場景理解:
繼承是你從爸爸那裡繼承了一整棟房子。房子很棒,但裡面附贈了一個你根本不需要的閣樓、一個會漏水的地下室,而且你沒辦法只繼承客廳。
組合是你去買樂高。要什麼拼什麼,每一塊你都知道自己為什麼拿它。組起來可能比較費工,但沒有一塊是你不知道為什麼在那裡的。
mixin 是「繼承」那一邊:你混入它,就得接受它帶來的全部東西,包括那兩個空函式和一個隱形依賴。
composable 是「樂高」那一邊:你只拿你解構出來的那幾個。
社群有一些工具號稱可以把 Options API 自動轉成 Composition API。我們沒有用。
原因很簡單:自動轉出來的東西,是「用新語法寫的舊程式碼」。
工具能做的是把 this.count 換成 count.value、把 methods 攤平成函式。但它不會幫你解決今天講的任何一個問題:
deep_clone 依賴,轉完之後還是隱形的(工具不知道它從哪來,多半會直接放棄或標記錯誤)換句話說:你花了力氣,得到一份長得比較新、但一樣難改的程式碼。
而 Composition API 的價值,九成來自「你被迫把依賴寫出來」這件事。如果你用工具跳過了這個過程,那你只是換了語法。
所以我們的做法是:一個一個重寫,而不是轉換。 慢很多,但每個檔案在重寫的過程中,都會有人被迫回答「這個東西到底從哪來」。
順帶一提,我們也沒有走「先升級到 Vue 2 的最後一版、裝上相容套件、再慢慢轉」這條中間路線。舊專案的版本停在 Vue 2.5,連 Vue 2 自己的最終版本都沒跟上,中間要補的東西也不少,與其分兩段走,不如一次到位。
<script setup> 一律用還有一個小決定,但影響很大:新專案的元件一律用 <script setup> 這種寫法。
理由只有一個:它讓「這個元件用到什麼」等於檔案最上方的 import 清單。
你打開一個檔案,往上捲到頂,就知道它依賴哪些東西。不需要搜尋、不需要翻 mixin、不需要問人。
這聽起來是小事,但對照今天那個 this.deep_clone(「答案就在檔案最上面」和「答案在另一個你不知道存在的檔案裡」),是完全不同的維護體驗。
this.$refs 怎麼辦?Day 2 量到舊專案有 330 次「父元件直接伸手進子元件呼叫方法」。
搬到 Vue 3 之後,這種寫法還是能用(語法變成 ref() 加上 defineExpose),而且有了型別,甚至更安全一點。
但我們處理的方式是重寫每一頁的時候問一句:為什麼需要這個 ref?不是逐句翻譯它。
答案通常是三種:
| 原本想做的事 | 更好的做法 |
|---|---|
| 呼叫子元件的「重新查詢」 | 把查詢條件變成 props,子元件自己 watch |
| 呼叫子元件的「清空表單」 | 把表單狀態拉到父層,或用 key 強制重建 |
| 拿子元件的「目前選取值」 | 用 v-model 或 emit 往上送 |
三種的共同點是:把「我知道你內部有什麼方法」換成「我們約定好要傳什麼資料」。
這正是 Day 2 那個借醬油的故事:從「自己開門進廚房翻櫃子」,改回「按門鈴說我要什麼」。
這個決定不是沒有成本,有三個很實在的:
一、慢很多。 一個一個重寫,比跑工具慢一個量級。這也是為什麼核心重構要花六個月(Day 5)。
二、<script setup> 加上 Composition API,程式碼不一定變短。 很多時候還變長了,因為原本靠 this 隱式拿到的東西,現在每一個都要寫出來。這是我們用「多打幾個字」換「看得見依賴」的交易,我覺得划算,但你要知道自己在買什麼。
三、寫法自由帶來新的不一致。 Options API 至少強迫大家把東西放在固定位置;Composition API 你想怎麼排都可以。如果沒有團隊約定,一個檔案裡的 ref 可能散落各處。 這件事我們後來是靠專案守則和 review 補的,不是靠框架。
this,那只是換了語法。this.deep_clone 能跑六年,純粹是因為運氣好。this.$refs 的時候,先問「為什麼需要它」,而不是「Vue 3 要怎麼寫」。 多數時候答案是不需要。明天 Day 8 是 Vue 2 → Vue 3 的下半場。我會從一個發現講起:舊專案的狀態管理註冊了 12 個模組,其中六個打開來只有兩行(一個空物件)。