EP37 - 可組件化的

閱讀時間約 23 分鐘
八個實用範例終於結束之後,又要繼續啃官方文件摟!
接下來文件著重於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 ~~
人生中 ~~遇到各種問題並解決它~


avatar-img
2會員
70內容數
分享生活趣事~
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
卡關的人生 的其他內容
Vue 的 scoped 樣式無法完全隔離全局樣式,需要採取其他策略如 BEM 命名規則或 CSS Modules 來避免衝突。未來可能也會遇到 JavaScript 衝突,因此需遵循嚴格的命名和範圍控制。
這段代碼展示了如何使用 Vue 的 <TransitionGroup> 組件來實現帶過渡效果的列表,這是一個實用的例子,對於學習前端動畫效果非常有幫助。
透過 Vue 3,可以輕鬆實現模態對話框。在 App.vue 中,使用按鈕來顯示模態視窗,並利用 Teleport 將其渲染到 body 中。在 Modal.vue 中,透過插槽提供自定義標題、主體和頁腳,並使用 Transition 組件添加顯示和隱藏的過渡效果。
引入 PolyGraph 子組件,並使用 ref 和 reactive 來管理標籤及其對應值。AxisLabel.vue 負責顯示每個統計數據標籤,並根據數據計算其在圖形中位置。PolyGraph.vue 則負責繪製多邊形和圓形,並透過 valueToPoint 函數將統計數據轉換為 SVG 坐標。
Tree View 是一種適合於建立父子組件關係的結構,能夠展示多層級的數據,特別適合遞回渲染。這個組件允許用戶雙擊項目將其轉換為文件夾,並通過 TreeItem 組件遞歸渲染子項目。
在 Vue 3 中使用計算屬性來實現表格的排序和篩選功能。通過創建一個可重用的表格組件(DemoGrid),可以輕鬆管理和顯示外部數據。
Vue 的 scoped 樣式無法完全隔離全局樣式,需要採取其他策略如 BEM 命名規則或 CSS Modules 來避免衝突。未來可能也會遇到 JavaScript 衝突,因此需遵循嚴格的命名和範圍控制。
這段代碼展示了如何使用 Vue 的 <TransitionGroup> 組件來實現帶過渡效果的列表,這是一個實用的例子,對於學習前端動畫效果非常有幫助。
透過 Vue 3,可以輕鬆實現模態對話框。在 App.vue 中,使用按鈕來顯示模態視窗,並利用 Teleport 將其渲染到 body 中。在 Modal.vue 中,透過插槽提供自定義標題、主體和頁腳,並使用 Transition 組件添加顯示和隱藏的過渡效果。
引入 PolyGraph 子組件,並使用 ref 和 reactive 來管理標籤及其對應值。AxisLabel.vue 負責顯示每個統計數據標籤,並根據數據計算其在圖形中位置。PolyGraph.vue 則負責繪製多邊形和圓形,並透過 valueToPoint 函數將統計數據轉換為 SVG 坐標。
Tree View 是一種適合於建立父子組件關係的結構,能夠展示多層級的數據,特別適合遞回渲染。這個組件允許用戶雙擊項目將其轉換為文件夾,並通過 TreeItem 組件遞歸渲染子項目。
在 Vue 3 中使用計算屬性來實現表格的排序和篩選功能。通過創建一個可重用的表格組件(DemoGrid),可以輕鬆管理和顯示外部數據。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
在本章節中,我們探討了 PHP 中如何引用和管理套件。學習了如何使用 Composer 來安裝第三方套件,以及如何引用自定義模組。此外,我們還介紹了如何創建和使用自定義套件,並列舉了一些在 PHP 社群中常見且廣泛使用的套件和庫。通過掌握這些知識,開發者可以更有效地管理和利用各種資源。
Thumbnail
切換頁面卡卡有很多種原因,這裡舉的例子只針對元件太大的情境。 除了想辦法拆分外,還有一個方法就是利用 Vue 的 Async Component。
Thumbnail
到底要用 ref 還是 reactive 是一個很常見的問題,不過現在官方文檔推薦使用 ref 就行,所以也不是甚麼大問題就是了。( •̀ ω •́ )✧ 所以 reactive 真的沒用用途了嗎?這篇文章來記錄一下 reactive 的用法。
Thumbnail
在 TypeScript 中,套件是模組化代碼的集合,可以提高代碼的可重用性和可維護性。常見的套件包括各種庫和框架,如 lodash、express 等。以下是有關引用套件、自定義套件和常見套件的詳細介紹。
Thumbnail
因為最近想嘗試編碼風格,於是就選了一套比較"不嚴格"的輔助工具來摸索。 編輯器 VS CODE 框架 VUE3 打包工具 VITE 編碼風格 Standard 環境 version { "nodejs":"v18.18.0", "npm":"9.8.1" }
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
我們在實作中,難免會遇到在不同組件中,卻有需求相同的資料格式,因此 mixins 可以達到我們的需求,除了 data 以外也包含了 methods 可以共用,舉例來說,學生資料可能會在,班級跟社團內被使用,當我們要撰寫元件時,就可以省略多餘的 data 定義。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
在本章節中,我們探討了 PHP 中如何引用和管理套件。學習了如何使用 Composer 來安裝第三方套件,以及如何引用自定義模組。此外,我們還介紹了如何創建和使用自定義套件,並列舉了一些在 PHP 社群中常見且廣泛使用的套件和庫。通過掌握這些知識,開發者可以更有效地管理和利用各種資源。
Thumbnail
切換頁面卡卡有很多種原因,這裡舉的例子只針對元件太大的情境。 除了想辦法拆分外,還有一個方法就是利用 Vue 的 Async Component。
Thumbnail
到底要用 ref 還是 reactive 是一個很常見的問題,不過現在官方文檔推薦使用 ref 就行,所以也不是甚麼大問題就是了。( •̀ ω •́ )✧ 所以 reactive 真的沒用用途了嗎?這篇文章來記錄一下 reactive 的用法。
Thumbnail
在 TypeScript 中,套件是模組化代碼的集合,可以提高代碼的可重用性和可維護性。常見的套件包括各種庫和框架,如 lodash、express 等。以下是有關引用套件、自定義套件和常見套件的詳細介紹。
Thumbnail
因為最近想嘗試編碼風格,於是就選了一套比較"不嚴格"的輔助工具來摸索。 編輯器 VS CODE 框架 VUE3 打包工具 VITE 編碼風格 Standard 環境 version { "nodejs":"v18.18.0", "npm":"9.8.1" }
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
我們在實作中,難免會遇到在不同組件中,卻有需求相同的資料格式,因此 mixins 可以達到我們的需求,除了 data 以外也包含了 methods 可以共用,舉例來說,學生資料可能會在,班級跟社團內被使用,當我們要撰寫元件時,就可以省略多餘的 data 定義。