2024-10-14|閱讀時間 ‧ 約 0 分鐘

EP37 - 可組件化的

八個實用範例終於結束之後,又要繼續啃官方文件摟!
接下來文件著重於Reusability (可重複使用性)
Composables通常是指可以重複使用的函數,
組件化是不是有什麼技巧呢?

提示

本節假設您已具備基本的 Composition API 知識。如果您一直在使用 Options API 學習 Vue,您可以將 API 偏好設置為 Composition API(使用官方文件上左側邊欄頂部的切換按鈕),然後重新閱讀「響應式基礎」和「生命週期鉤子」章節。
網誌裡頭都是介紹Composition API為主摟!

什麼是「Composable」?

在 Vue 應用程式的上下文中,「composable」是一個利用 Vue 的 Composition API 封裝和重用有狀態邏輯的函式。

在構建前端應用程式時,我們經常需要重用一些常見任務的邏輯。例如,我們可能需要在很多地方格式化日期,因此我們會提取出一個可重用的函式。這個格式化函式封裝了無狀態的邏輯:它接收一些輸入並立即返回預期的輸出。有許多庫可以用來重用無狀態的邏輯,例如 lodashdate-fns,這些你可能聽說過。

相比之下,有狀態的邏輯涉及管理隨時間變化的狀態。簡單的例子是跟蹤頁面上鼠標的當前位置。在實際場景中,它還可能是更複雜的邏輯,如觸摸手勢或數據庫連接狀態。

鼠標跟蹤示例 - Mouse Tracker Example

如果我們使用 Composition API 直接在組件內實現鼠標跟蹤功能,可能會像這樣:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
x.value = event.pageX
y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

但是如果我們想在多個組件中重用相同的邏輯,我們可以將邏輯提取到一個外部文件中,作為一個 composable 函式:

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按慣例,composable 函式名稱以 "use" 開頭
export function useMouse() {
// composable 封裝和管理的狀態
const x = ref(0)
const y = ref(0)

// composable 可以隨時間更新其管理的狀態
function update(event) {
x.value = event.pageX
y.value = event.pageY
}

// composable 也可以鉤入其擁有者組件的生命週期,設置和拆除副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

// 暴露管理的狀態作為返回值
return { x, y }
}

這就是它在組件中的使用方式:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

Try it in the playground

我們可以看到,核心邏輯保持不變 - 我們所做的只是將其移到一個外部函式中並返回應暴露的狀態。就像在組件內一樣,你可以在 composables 中使用 Composition API 的全部功能。現在,這個 useMouse() 功能可以在任何組件中使用。

Composables 更酷的部分是你還可以嵌套它們:一個 composable 函式可以調用一個或多個其他的 composable 函式。這使我們能夠使用小的、獨立的單元來組合複雜的邏輯,類似於我們使用組件組合整個應用程式。事實上,這就是我們將這種模式下的 API 集合稱為 Composition API 的原因。

例如,我們可以將添加和移除 DOM 事件監聽器的邏輯提取到自己的 composable 中:

// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
// 如果你願意,你還可以讓它支持選擇器字符串作為目標
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}

現在,我們的 useMouse() composable 可以簡化為:

// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
const x = ref(0)
const y = ref(0)

useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})

return { x, y }
}

提示

每個調用 useMouse() 的組件實例都會創建自己的 xy 狀態副本,這樣它們就不會互相干擾。如果你想在組件之間管理共享狀態,請閱讀「狀態管理」章節。

非同步狀態示例 - Async State Example

useMouse() composable 不接受任何參數,現在我們來看看另一個示例,它會使用一個參數。在進行非同步數據獲取時,我們經常需要處理不同的狀態:加載中、成功和錯誤:

<script setup>
import { ref } from 'vue'

const data = ref(null)
const error = ref(null)

fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>

<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>

在每個需要獲取數據的組件中重複這種模式會非常繁瑣。讓我們將其提取到一個 composable 中:

// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
const data = ref(null)
const error = ref(null)

fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))

return { data, error }
}

現在在我們的組件中,我們可以這樣做:

<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

接受反應式狀態 - Accepting Reactive State

useFetch() 接受一個靜態的 URL 字符串作為輸入 - 因此它只執行一次抓取,然後完成。如果我們希望它在 URL 更改時重新抓取數據該怎麼辦?為了實現這一點,我們需要將反應式狀態傳遞到 composable 函式中,並讓 composable 創建監聽器來使用傳遞的狀態執行操作。

例如,useFetch() 應該能夠接受一個 ref

const url = ref('/initial-url')

const { data, error } = useFetch(url)

// 這應該會觸發重新抓取
url.value = '/new-url'

或者,接受一個 getter 函式

// 當 props.id 更改時重新抓取
const { data, error } = useFetch(() => `/posts/${props.id}`)

我們可以使用 watchEffect()toValue() API 重構我們的現存實現:

// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
const data = ref(null)
const error = ref(null)

const fetchData = () => {
// 在抓取前重置狀態..
data.value = null
error.value = null

fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}

watchEffect(() => {
fetchData()
})

return { data, error }
}

toValue() 是在 Vue 3.3 中新增的 API,用於將 refs 或 getter 標準化為值。如果參數是 ref,它會返回 ref 的值;如果參數是一個函數,它會調用該函數並返回其返回值。否則,它將按原樣返回參數。這與 unref() 類似,但對函數有特殊處理。

請注意,toValue(url) 在 watchEffect 回調內被調用。這確保了在 toValue() 標準化過程中訪問的任何響應式依賴項都會被監視器追踪。

這個版本的 useFetch() 現在接受靜態 URL 字串、refs 和 getter,使其更加靈活。監視效果將立即運行,並將追踪在 toValue(url) 過程中訪問的任何依賴項。如果沒有追踪到依賴項(例如,url 已經是一個字串),效果只會運行一次;否則,每當追踪的依賴項更改時,效果將重新運行。

以下是更新版本的 useFetch(),包含一個人工延遲和隨機錯誤示範:

命名規範與最佳實踐 - Conventions and Best Practices

命名 - Naming

習慣上,可組合函數的名稱應使用 camelCase 並以 "use" 開頭。

輸入參數 - Input Arguments

可組合函數可以接受 ref 或 getter 作為參數,即使它們不依賴於它們的響應式特性。如果您正在編寫一個可能由其他開發人員使用的可組合函數,處理輸入參數可能是 refs 或 getter 而非原始值是個好主意。toValue() 實用函數在這種情況下非常有用:

import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是 ref 或 getter,
// 則返回其標準化值。
// 否則,按原樣返回。
const value = toValue(maybeRefOrGetter)
}

如果您的可組合函數在輸入是 ref 或 getter 時創建了響應式效果,請確保使用 watch() 明確監視 ref / getter,或在 watchEffect() 內調用 toValue() 以確保其被正確追踪。

我們之前討論的 useFetch() 實現提供了一個接受 refs、getter 和純值作為輸入參數的具體範例。

返回值 - Return Values

您可能已經注意到,我們在可組合函數中一直使用 ref() 而非 reactive()。推薦的習慣是,可組合函數應始終返回一個包含多個 refs 的純非響應式物件。這允許在組件中解構同時保留響應性:

// x 和 y 是 refs
const { x, y } = useMouse()

從可組合函數返回響應式物件會導致這樣的解構失去與可組合函數內部狀態的響應性連接,而 refs 將保持這種連接。

如果您更喜歡將可組合函數返回的狀態作為物件屬性使用,您可以用 reactive() 包裝返回的物件,這樣 refs 就會被解構。例如:

const mouse = reactive(useMouse())
// mouse.x 與原始 ref 連接
console.log(mouse.x)

模板:

滑鼠位置是:{{ mouse.x }}{{ mouse.y }}

副作用 - Side Effects

在可組合函數中執行副作用(例如添加 DOM 事件監聽器或獲取數據)是可以的,但請注意以下規則:

如果您正在開發使用服務端渲染(SSR)的應用程式,請確保在掛載後的生命週期鉤子(例如 onMounted())中執行特定於 DOM 的副作用。這些鉤子只會在瀏覽器中調用,因此您可以確定其中的代碼可以訪問 DOM。

請記得在 onUnmounted() 中清理副作用。例如,如果一個可組合函數設置了一個 DOM 事件監聽器,應該在 onUnmounted() 中移除該監聽器,就像我們在 useMouse() 示例中看到的那樣。使用像 useEventListener() 這樣的可組合函數自動完成這個過程是個好主意。

使用限制 - Usage Restrictions

可組合函數應僅在 <script setup> 或 setup() 鉤子中調用。它們也應在這些上下文中同步調用。在某些情況下,您也可以在生命週期鉤子中調用它們,例如 onMounted()。

這些限制很重要,因為這些是 Vue 能夠確定當前活躍組件實例的上下文。訪問活躍組件實例是必要的,以便:

  • 將生命週期鉤子註冊到它。
  • 將計算屬性和監視器鏈接到它,這樣當實例被卸載時它們可以被處置,以防止內存洩漏。

提示

<script setup> 是唯一可以在使用 await 之後調用可組合函數的地方。編譯器會在異步操作後自動恢復活動實例上下文。-

提取可組合函數以組織代碼 - Extracting Composables for Code Organization

可組合函數不僅可以用於重用,還可以用於代碼組織。隨著組件複雜性的增加,你的組件可能會變得過於龐大,難以導航和理解。組合 API 給你完全的靈活性,將組件代碼根據邏輯關注點組織成更小的函數:​

<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

在某種程度上,你可以將這些提取的可組合函數視為可在組件範圍內相互通信的服務。

在 Options API 中使用可組合函數 - Using Composables in Options API​

如果你使用 Options API,可組合函數必須在 setup() 中調用,並且返回的綁定必須從 setup() 返回,這樣它們才能暴露給 this 和模板:

import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
setup() {
const { x, y } = useMouse()
const { data, error } = useFetch('...')
return { x, y, data, error }
},
mounted() {
// setup() 暴露的屬性可以通過 `this` 訪問
console.log(this.x)
}
// ...其他選項
}

與其他技術的比較 - Comparisons with Other Techniques

與 Mixins 的比較 - vs. Mixins

來自 Vue 2 的用戶可能對 mixins 選項並不陌生,它也允許我們將組件邏輯提取為可重用的單元。然而,mixins 有三個主要缺點:

  • 屬性來源不明:當使用許多 mixins 時,哪個實例屬性是由哪個 mixin 注入的變得不明,這使得追蹤實現和理解組件行為變得困難。這也是為什麼我們建議對可組合函數使用 refs + 解構模式:它使得在消費組件中屬性的來源變得清晰。
  • 命名空間衝突:來自不同作者的多個 mixins 可能會註冊相同的屬性鍵,導致命名空間衝突。使用可組合函數時,如果有來自不同可組合函數的衝突鍵,可以重新命名解構的變數。
  • 隱式跨 mixin 通信:需要互相交互的多個 mixins 必須依賴共享的屬性鍵,使它們隱式耦合。使用可組合函數時,從一個可組合函數返回的值可以作為參數傳遞給另一個可組合函數,就像普通函數一樣。

基於以上原因,我們不再建議在 Vue 3 中使用 mixins。此功能僅保留作為遷移和熟悉的原因。

與 Renderless Components 的比較

在組件插槽章節中,我們討論了基於範圍插槽的無渲染組件模式。我們甚至使用無渲染組件實現了相同的鼠標跟踪示例。

可組合函數相對於無渲染組件的主要優勢在於可組合函數不會產生額外的組件實例開銷。在整個應用程序中使用時,無渲染組件模式所創建的額外組件實例數量可能會變得明顯影響性能。

建議在重用純邏輯時使用可組合函數,而在重用邏輯和視覺佈局時使用組件。

與 React Hooks 的比較

如果你有 React 的經驗,你可能會注意到這看起來與自定義的 React hooks 非常相似。Composition API 部分受到 React hooks 的啟發,而 Vue 可組合函數在邏輯組合能力上確實與 React hooks 相似。然而,Vue 可組合函數基於 Vue 的細粒度響應系統,這與 React hooks 的執行模型根本不同。這在 Composition API FAQ 中有更詳細的討論。

進一步閱讀

  • 深入了解響應性:了解 Vue 的響應系統如何運作的低層次理解。
  • 狀態管理:管理多個組件共享狀態的模式。
  • 測試可組合函數:有關單元測試可組合函數的提示。
  • VueUse:一個不斷增長的 Vue 可組合函數集合。其源代碼也是一個很好的學習資源。
每次實作完,才會真的對各種知識慢慢地了解~
覺得邏輯才是最重要的 www ~~
人生中 ~~遇到各種問題並解決它~


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.