# 6-1 Composition API 簡介
作為 Vue.js 3.0 開始納入的重大特性,Composition API 在這之中所代表的地位絕對是舉足輕重, 這也是為什麼我選擇將此特性獨立出單一章節的原因。 那麼在本書的最後一個章節,我們就來看看這個 Vue Composition API 到底為 Vue.js 帶來了什麼變革。
# Composition API 是什麼
從 2018 年開始,Vue 3.0 的新計畫就已經在核心團隊開始醞釀了, 起初 Vue.js 團隊所採用的是類似 React 語法的 Class API - Vue Class Component (https://github.com/vuejs/vue-class-component (opens new window)),但在新版本考慮到與原有 API 的相容性、在 TypeScript 與非 TypeScript 開發者間的平衡 (看看 Angular 2+,不用 TS 無法開發)、瀏覽器對原生 ES Class 的支援度、透過 Class API 開發所需要的 Class fields、Decorators 等等提案始終還不穩定 (stage < 4) 等因素, 因此最終決定放棄 Class API 的路線。
在決定放棄 Class API 的路線之後,取而代之的是 Function-based API (也就是現在的 Composition API)。 Composition API 從 2019 年以 Function-based APIs 之名發表就引發社群熱議不斷, 直到 2020 年才正式定名為 Composition API,其由來自於「組合代替繼承」之意。
說得再白話一些,Composition API 就是為了要解決過去在 Vue.js 2.x 時,元件與元件之間的程式碼與邏輯結構,難以抽取出來重複使用的問題。而 Composition API 所提供的解決方式,就是以函式作為邏輯的載體,將該放在一起的東西放在一起。
什麼意思? 別急,相信看完本章,讀者們就能夠理解了。
# Vue 2.x - Options API 出了什麼問題
標題有些誤導人了,其實 Vue 2.x 很好,就像我們在前面幾個章節所介紹的那樣,我們可以把程式組織成一個個的「元件」,
這個元件同時包含了「模板」 (<template>
) 、「行為與邏輯」 (<script>
) ,以及「樣式」 (<style>
) 三大部分。
在 <script>
裡面,我們又可以將不同的屬性做切分,例如與模板響應式的狀態我們可以放在 data
裡面,相依的狀態放在 computed
,
事件與方法可以放在 methods
,以及生命週期鉤子函式,還有其他各式屬性等等。
像這樣,我們將元件內程式碼分門別類,看起來很棒。
但是隨著時間經過,過去開發的專案一定會面臨功能的新增、需求變更等等問題,這個時候,我們就會在原有的結構這樣加入新的功能:
於是,當元件的功能越來越多,明明應該是同一個操作邏輯,程式碼卻要散佈在元件的各種地方,時間一長不但不利於維護, 往後當我們想要重複使用某些功能的時候,卻也難以拆開了。
# 功能與邏輯的重複使用 (reusable)
而 Vue 2.x 當時對於功能與邏輯的重複使用, Vue.js 提供了幾種解決方案:
小提醒
本書範例使用的都是 Vue 3.x 的語法,可能與 Vue 2.x 的 API 會有些許不同,無法直接套用。
# 自定義指令 (Vue 3.0 仍支援,可安心使用)
如同我們在第一章曾介紹過的,指令就是 Vue.js 提供給模板使用的特殊屬性。 除了 Vue.js 內建的各種指令外,它也允許開發者自由建立新的指令。
舉例來說,過去我們會在 <img>
標籤上面加上 @error="..."
來處理當圖片載入錯誤時,用預設圖替代以免網頁破圖跑版的問題,
但是如果這個需求在多個元件上都需要用到,豈不是要在每個元件上都寫上這個方法?
這時,我們就可以自行建立一個指令來處理這個問題,建立一個客製化的指令很簡單,我們可以透過 app.directive()
:
假設就叫它 img-fallback
好了。
// 建立新元件
const app = Vue.createApp({ });
// 在 app 註冊自定義指令
app.directive('img-fallback', {
// mounted hook
mounted (el) {
// 傳入的 el 指的是元素本身
// 當 el 觸發 error 事件的時候,把 src 路徑換成預設圖
el.addEventListener('error', () => {
el.src = 'https://dummyimage.com/150x150/000/ffffff.png&text=no+image'
});
}
});
app.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
那麼實際應用的時候,我們就可以在所有想要設定預設圖的 <img>
標籤加上 v-img-fallback
的自定指令:
<div id="app">
<!-- 這張圖不存在 -->
<img
v-img-fallback
src="https://www.google.com/noimage.png" alt="這張圖不存在">
</div>
2
3
4
5
6
像這樣,當瀏覽器試圖取德這個不存在的圖檔 https://www.google.com/noimage.png
時,就會觸發 error 事件,
這個時候就會觸發我們在指令定義好的 error handler 把圖檔路徑至換成預設的路徑了。
自訂指令除了上面所示範的 mounted
hook 函式之外,這裏把全部 hook 列出來給讀者參考:
created
: 在綁定元素的屬性或事件監聽器前觸發。 (時間點在v-on
以前)beforeMount
: 指令剛被綁定到指定元素,且掛載到父元件之前觸發。mounted
: 指令綁定的元素,掛載到父元件之後觸發。beforeUpdate
: 該元件的 VNode 更新之前觸發。updated
: 該元件的 VNode 以及它的以元件被更新之後觸發。beforeUnmount
: 元素卸載前觸發。unmounted
: 元素卸載後觸發。
而對應的 callback 函式除了範例裡的 el
參數可以取到元素本身之外,同時還有 binding
、vnode
和 prevNnode
可以使用。
比較值得一提的是,binding
提供了這幾種屬性:
instance
: 使用此指令的元件實體。value
: 傳遞給指令的綁定值。oldValue
: 綁定值的前一個值,只能在beforeUpdate
或updated
的 hook 使用。arg
: 傳給此指令的參數。modifiers
: 修飾子,提供開發者自行定義。dir
: 一個物件,在指令被註冊的時候作為參數傳遞。
vnode
與 prevNnode
則是使用此指令的元件的虛擬節點 (即 virtual DOM) 與其前一個虛擬節點。
除了前面所介紹的預設圖之外,像是在元件內紀錄 Google Analytics 之類的事件追蹤,也很適合用自訂指令來處理:
// 列出所有需要被追蹤的事件名稱
const availableEventsList = [
'click',
'dblclick',
'input',
'keydown',
'keypress',
'keyup',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'scroll'
];
// 在 app 註冊 event-track 指令
app.directive('event-track', {
mounted(el, binding) {
const { value, modifiers } = binding;
// 當指定在 availableEventsList 裡的事件被觸發的時候
// 呼叫 event_track() 進行紀錄
for (const event in modifiers) {
if (availableEventsList.includes(event)) {
el.addEventListener(event, event_track(value.category, event, value.label));
break;
}
}
},
// 卸載前解除事件綁定
beforeUnmount(el, binding) {
const { value, modifiers } = binding;
for (const event in modifiers) {
if (availableEventsList.includes(event)) {
el.removeEventListener(event, event_track(value.category, event, value.label));
break;
}
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
實際在模板元素上就可以使用 v-event-track
這個指令:
<!-- 將追蹤事件名放在修飾子上, category 與 label 則是透過 binding.value 取得 -->
<button v-event-track.click="{ category: '...', label: '...' }"> click </button>
2
這樣一來,當這個按鈕被點擊的時候,就會呼叫 event_track
來記錄相關事件了。
# mixins (Vue 3.0 仍支援,但已不建議使用)
除了自定義指令之外,Vue.js 也提供了 Mixins
的語法來提供不同元件內的屬性重複使用,先自訂一個 Mixin 物件:
// 自訂一個 Mixin 物件
const myMixin = {
methods: {
hello() {
console.log('hello from mixin!')
}
},
created() {
this.hello()
},
};
2
3
4
5
6
7
8
9
10
11
需要使用的時候,在元件加上 mixins
屬性:
const app = Vue.createApp({});
// 子元件 A
app.component('custom-component-a', {
mixins: [myMixin]
});
// 子元件 B
app.component('custom-component-b', {
mixins: [myMixin]
});
app.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
像這樣,我們 custom-component-a
與 custom-component-b
同時透過 mixins
將 myMixin
物件引入。
引入 myMixin
物件後的 custom-component-a
與 custom-component-b
元件,
就會分別同時擁有 hello()
以及 created
hook 的功能了,意義等同於下面段程式碼:
// 子元件 A
app.component('custom-component-a', {
methods: {
hello() {
console.log('hello from mixin!')
}
},
created() {
this.hello()
},
});
// 子元件 B
app.component('custom-component-b', {
methods: {
hello() {
console.log('hello from mixin!')
}
},
created() {
this.hello()
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
如果有多個 mixins
則可利用 mixins: [myMixinA, myMixinB],
逗號隔開的方式加入。
像這樣,我們將相同的屬性拆出來到 mixins 物件後,再將它分別引進到需要繼承的元件內,是不是很方便呢!
但是 mixins
一時爽,一直 mixins
的結果?
前面我們說過,專案的程式碼通常 就像工程師的體重一樣 會隨著時間與需求增加、修改而不斷地成長。
當我們的程式碼經過不斷地迭代,累積了代代祖傳的各種 mixins
時,就是開發者崩潰的開始。
我用個簡單的例子示範:
// myMixinA
const myMixinA = {
data () {
return {
msg: 'Hello'
}
},
mounted() {
alert(`${this.msg}, ${this.name}!!`);
}
};
// myMixinB
const myMixinB = {
data () {
return {
msg: '你好'
}
},
};
// myMixinC
const myMixinC = {
data () {
return {
name: '訪客'
}
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
這個時候問題來了,請問各位讀者,以下的兩個元件在執行的時候,分別會發生什麼事?
// 子元件 A
app.component('custom-component-a', {
template: `<h1> {{msg}}, {{name}}!! }} </h1>`,
mixins: [myMixinA],
data () {
return {
name: 'Kuro'
}
}
});
// 子元件 B
app.component('custom-component-b', {
template: `<h1> {{name}}, {{msg}}!! }} </h1>`,
mixins: [myMixinB, myMixinC],
});
// 子元件 C
app.component('custom-component-c', {
template: `<h1> {{name}}, {{msg}}!! }} </h1>`,
mixins: [myMixinA, myMixinB],
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
相信各位讀者也很難在第一時間回答這個問題對吧。
像這樣,在元件之間充滿各種不同 mixins
的排列組合,發生了什麼問題?
可能前一位開發者看到了 custom-component-b
能夠正常運作,就仿造前面的寫法寫了一個 custom-component-c
,
怎知道 myMixinA
與 myMixinB
裡面根本沒有定義 name
這個 data
屬性,於是在模板 template
要印出 name
時就會出現問題。
邏輯與結構「過度」封裝的結果,就是開發的時候,我們根本不知道這個元件內哪些狀態、哪些方法是可以被調用的,甚至是有哪些會衝突,我們應該要避開的,完全無從得知。
而本章所要介紹的 Vue 3.0 Composition API 就是為了解決這個問題所發展出來的。
# Composition API (Vue 3.0 新增)
自 Vue 3.0 引進了 Composition API 這個特性後,「表面上看起來」與前面所介紹的 mixins
感覺很像:
都是將跨元件共用的屬性 (如 data
、computed
、methods
... 等) 包裝起來,然後需要用的元件再它們引入進去。
但是與 mixins
有個很大的不同之處是,被引入 (import) 到元件內的屬性,必須要以「物件」或「函式」的形式來將它們引進到元件中。 這裏我們以一個簡單的 todo-list 來示範。
這裏我們建立一個名為 toDo
的函式,將狀態 (原本在 data
回傳的東西) 透過 Vue.ref()
進行封裝。
另外新增 add
與 remove
用來處理增加與移除 items
陣列元素的方法,最後再將狀態、方法透過 return
回傳出去:
// todo function
const toDo = () => {
const todo = Vue.ref('');
const items = Vue.ref(['Vue', 'is', 'Awesome']);
// 將輸入框新增到列表中
const add = () => {
if (todo.value) {
items.value.push(todo.value);
todo.value = '';
}
};
// 將選中的項目移除
const remove = item => {
items.value = items.value.filter(v => v !== item);
};
// 這裏只需要回傳 <template> 會用到的部分即可
return {
todo,
items,
add,
remove
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
這個時候,我們只需要在子元件 todo-list
裡面使用 setup
來啟動這個元件:
const app = createApp({
setup() {
// 這裏沒有 this!
const {
todo,
items,
add,
remove
} = todoList();
// 將模板會用到的部分 return 出去
return {
todo,
items,
add,
remove
};
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
而網頁模板的部分則是與過去的撰寫風格一樣:
<h3>Todo App</h3>
<div class="todo-list">
<div>
<input v-model="todo" @keyup.enter="add" type="text">
<button @click="add">Add</button>
</div>
<ul class="list-group">
<li v-for="item in items" :key="item">
<span>{{ item }}</span>
<button @click="remove(item)">
<span>×</span>
</button>
</li>
</ul>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
日後若想加入新功能,假設新增一個 counter
的計數器按鈕,
所以我們增加一個 counter
函式,並回傳計數數字與 add
方法:
const counter = () => {
const count = ref(0);
const add = () => {
count.value++;
};
return {
count,
add
}
};
2
3
4
5
6
7
8
9
10
11
於是主元件程式便可以這樣改寫:
const app = createApp({
setup() {
// 這裏沒有 this!
const {
todo,
items,
add,
remove
} = todoList();
// 因為 add 與上面 todoList 的 add 名稱重複了,所以換個名字 counterAdd
const {
count,
add: counterAdd,
} = counter();
// 將模板會用到的部分 return 出去
return {
todo,
items,
add,
remove,
count,
counterAdd
};
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
像這樣我們就可以將 todo-list
與 counter
的邏輯分別拆分出來,利用新的 Composition API
來進行開發,
未來引入到元件使用的時候也不再會有過去混淆不清的情況了。
但這裡必須強調,Composition API 絕對不是為了廢棄原本 Options API 所發展出的語法,它只是 Vue.js 官方團隊提供給開發者們的另一種組織程式碼的風格。
針對 Composition API 的基本介紹就到這,在接下來的小節裡面,我們將繼續介紹 Composition API 各種常用到的 API 以及使用方法。