iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 19

Day 19: Vue - 組合式邏輯概念 (Vue composable concept)

  • 分享至 

  • xImage
  •  

昨天複習了工廠函式,今天來連結到Vue滿常見的設計模式-組合式邏輯(Composable)

Composable 是一個相對通俗的概念,通常指的是可以在多個元件之間重複使用的邏輯模塊,並且結合了 Vue 3 的組合式 API(Composition API)來實現。

Composable通常利用 Vue 的響應式系統和生命週期鉤子,讓程式碼更加模組化、彈性化和易於維護。這不僅提高了邏輯的可重用性,也有效避免了元件檔案因為業務邏輯過於複雜而變得過於龐大,有助於保持程式碼的清晰和結構化。

今日學習目標:

  1. 理解Composable和一般JavaScript通用函式差別
  2. Composable基本用法和案
  3. Composable設計步驟和注意事項

Composable vs Utilities 差別

Composable (組合式函式)

是 Vue 3 新的特性,主要用在多個组件之間的重複性共享邏輯,透過 Vue 提供的 API 來封裝如狀態管理、副作用(watch)、計算屬性(compute)等,

像是能夠存取和管理 Vue 的響應式系統、可以包含例如 API 請求、訂閱等,也可以和 Vue 的生命週期鉤子(如 onMounted、onUnmounted 等)結合使用

Utility (通用型函式)

Utility 通常是指在程式碼中執行特定任務或轉換資料的純函數,它們與 Vue 所提供的API無關

通常是與框架無關的通用函數,獨立於Vue 的響應式系統和生命周期鉤子,可以在任何 JavaScript 執行環境中使用,不限於 Vue.js (不帶有Vue 提供的API)。

通常用於資料轉換、格式化、簡單計算等,因為是純函数,意味著给定相同的輸入值,總是返回没有副作用的輸出,設計上有利於單元測試。

// 將日期格式化為 YYYY-MM-DD
export function formatDate(date) {
  const d = new Date(date);
  const year = d.getFullYear();
  const month = ('0' + (d.getMonth() + 1)).slice(-2);
  const day = ('0' + d.getDate()).slice(-2);
  return `${year}-${month}-${day}`;
}

總結歸納來說,如果函式中使用了 Vue 才有的功能 (例如 ref 或是 onMounted),我們就會稱它為組合式函式而不是普通的函式。


composable 簡單案例

計數器 Composable 案例

我們可以簡單創建一個 useCounter 函式,用來管理計數器邏輯。函式返回值包括計數器的響應式資料狀態和一些更新資料邏輯的函式,比如增加、減少和重置計數等功能函式。

// composables/useCounter.js
import { ref } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  const increment = () => {
    count.value++;
  };

  const decrement = () => {
    count.value--;
  };

  const reset = () => {
    count.value = initialValue;
  };

  return {
    count,
    increment,
    decrement,
    reset,
  };
}

// 元件中引用使用
<template>
  <div>
    <p>每一個組件的計數:{{ count }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script setup>
import { useCounter } from '@/composables/useCounter';
</script>

其實跟昨天複習工廠函式有提到,是 JavaScript 模組執行作用域的典型應用

  • 核心觀念

useCounter 這類的 Composable 函數每次被呼叫時,都會透過回傳值建立一個新的閉包環境(closure),裡面封存著當下執行環境(execution context)下使用過的資料,從而產生獨立的狀態實例

當你在多個元件中呼叫同一個 Composable(如 useCounter),每個元件都會獲得一個獨立的計數器實例。這是因為每次呼叫 Composable 函數時,都會建立一個新的執行上下文和狀態(如 ref、reactive 等)

這些狀態被封閉在獨立執行環境(execution context),並且只有在對應的元件中使用。即使多個元件重複使用了同一個邏輯,它們之間的資料流仍然是獨立的,不會互相干擾。每個元件都有自己的狀態副本。


composable 函式設定步驟和注意事項

如果以昨天複習過的工廠函式的思維,怎麼定義內部變數和決定要公開的方法或資料狀態應該滿類似的:

開發設計

  • 命名以use開頭
  • 是否需要接收參數(paramter)
  • 定義內部響應式狀態資料
  • 決定那些操作邏輯需要返回暴露使用(公開和私有狀態、方法)
  • 返回值可以包括響應式資料,最好以ref定義響應式變數

注意事項

  • 多個 composable 引用進入同一元件時,注意命名衝突
  • 瀏覽器事件偵聽器記得在元件卸載時移除

搭配watcher 接收url為參數,非同步fetch api composable案例:

import { ref, watch, toValue } from 'vue'
// 當改變 url時 composable function會重新fetch api
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // Reset state before fetching...
    data.value = null
    error.value = null

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

  // Use watch to explicitly monitor changes to url
  watch(url, () => {
    fetchData()
  }, { immediate: true }) // Immediate option to trigger fetch on initial setup

  return { data, error }
}
  • 決定那些操作邏輯需公開或私有狀態

compsable是由回傳值去決定那些操作方法或內部資料你需要暴露給使用者使用,當然如果覺得某些資料很重要暴露出去給畫面渲染,但又不希望被更動到,可以選擇用readonly鎖住。

通常實務上自己設計會將暴露的出去的資料另外設計update function,使用者在更新資料時更加有意識地使用 update 函數,而不是變動原始資料。

import { ref, readonly } from 'vue';

export function useCounter() {
  // 定義內部可變動的狀態
  const count = ref(0);

  // 使用 `readonly` 將 count 鎖住,避免外部直接修改
  const readonlyCount = readonly(count);

  // 更新函數,提供修改 count 的介面
  const updateCount = (value) => {
    // 可以做一些簡單型別檢查 也可以使用customRef 看資料結構複雜度
    if (typeof value === 'number') {
      count.value = value;
    } else {
      console.warn('Invalid value, count must be a number');
    }
  };

  // 回傳 readonly 的 count 和更新函數
  return {
    count: readonlyCount, // 這裡暴露出去的是 readonly 版本的 count
    updateCount // 這裡提供的是修改的途徑
  };
}

  • 返回值盡量用ref

composable返回值,建議是以 ref 物件單獨放在一個普通物件裡返回,因為上次有提到reactive單獨解構會回傳純值,喪失響應式

為了避免解構 reactive 物件時失去響應性,通常會使用一個特殊的API toRefs 將 reactive 物件的屬性轉換為 ref,這樣即使解構後屬性仍然是響應式的,不過還是建議composable 函式還是盡量以 ref當作返回值為主,開發複雜度比較低。

Vue-toRef介紹

import { reactive, toRefs } fro

export function useExample() {
  const state = reactive({
    count: 0,
    message: 'Hello',
  });

  // 如果直接解構reactive,若解構出來的是primitive type 只會返回該數值,不會再掛入proxy
  const { count, message } = toRef(state);

  return {
    count,
    message,
  };
}

  • Vue元件外部系統需要自行卸載(瀏覽器監聽器等)

如果你在一個 composable 中使用了 watch 或 watchEffect 偵聽器,通常不需要手動卸載它們。 Vue 會自動管理這些偵聽器的生命週期,元件被卸載時(onMounted)與元件相關的偵聽器也會自動被清理

  • window object偵聽器:

如果你在 composable 中使用了與元件資料流無關偵聽器(如 window 物件上的事件),你需要手動清理這些偵聽器。這種情況並不會自動管理,因為它們超出了 Vue 的生命週期管理範圍,是屬於非框架資料流外(uncontrolled data flow),類似上次說的非受控現象,記得同步隨元件消毀移除監聽器。

 // 在組件掛載時,同步設定window監聽器
  onMounted(() => {
    window.addEventListener('resize', handleResize);
  });

  // 在組件卸載時,也必須同步卸載window監聽器,避免記憶體洩漏
  onUnmounted(() => {
    window.removeEventListener('resize', handleResize);
  });

composable函式重用性(reusable)不是唯一需要考量的點

這是我在面對實務開發上,分解商業需求常遇到的,我們製作composable設計上往往想說,要盡量讓它的重複邏輯利用性達到最高(變得更完美,能夠相容各種需求),不過實際開發上滿常遇到業務邊際需求:

90%的邏輯大家是都是類似,但有10%是客製化需求方想要的。

最近滿喜歡的一種思維,沒辦法共用,就別強迫自己綁在共用compoasble函式裡,即便他們有90%相似~

原文出處

很多開發人員在面對新功能時會選擇修改既有的組合式函式而不是製作一個新的,只為了讓他能夠在更多元件中被使用。在這種情況下,我們常常見到組合式函式「失控」 —為了能處理各種(邊緣)情況,越來越多的參數和方法被加入,導致事情遠比它應有的還要複雜;而且隨著時間推移,重構/替換疊加的成本只會越來越高。若您發現舊的組合式函式開始變得過於複雜,不要害怕建立新的組合式函式。

若某個元件的商業邏輯有點複雜,甚至很特殊,即使整個應用程式中只有一個元件在使用這個功能,將這個特殊(奇怪)的功能「切」(模組化) 成數個小功能 (組合式函式) 分離出來是完全沒問題的。

因為通常組合式函式會放在全域composable資料夾底下,在開發設計上往往會以相容給其他組件使用為主,不過某些頁面(page)層元件,包裹的商業邏輯比較特殊,我們設計共用性時常遇到困難~

所以在某需特殊商業邏輯沒那麼共用情況下,可以選擇在pages資料夾底下開發新的屬於該頁面的composables函式

/src
├── composables/
│   └── useGlobalFeature.js // 全局大家可以共用的
├── pages/
│   ├── PageA/
│   │   ├── PageA.vue
│   │   └── usePageALogic.js // 特殊商業邏輯
│   └── PageB/
│       ├── PageB.vue
│       └── usePageBLogic.js  // 特殊商業邏輯

總結

觀念

Composable 是 Vue 3 中的重要概念,旨在提供一種模組化、彈性化的方式來重用邏輯。相比傳統的 JavaScript 通用函式,組合式函式能夠整合 Vue 的響應式系統和生命週期鉤子,使開發者能夠更有效地處理元件間的狀態共享、非同步請求、和副作用處理

使用

設計組合式函式時,應該考慮命名規則、參數設置、狀態封裝,以及暴露的操作方法,也可以透過 readonly 保護重要的資料不被意外修改。

設計思維

組合式函式的目標並不僅僅是考量重用邏輯,還需要考慮可維護性和彈性。遇到特定的邊緣需求時,不應勉強將所有邏輯納入共用的組合式函式,而是可以根據業務場景拆分出多個功能模塊來處理特殊需求。


學習資源

  1. https://piesdoc.com/zh-Hant/docs/vue3/composables/
  2. https://enterprisevue.dev/blog/understanding-composables-in-vue/
  3. https://www.patterns.dev/vue/composables/
  4. https://hackmd.io/@SkT7-27LSWWQi5G2DJBLkw/HkbaWiHljhttps://learnvue.co/articles/composition-api-reusability#reusing-logic
  5. https://vueschool.io/articles/vuejs-tutorials/how-to-write-a-vue-composable-step-by-step/
  6. https://learnvue.co/articles/composition-api-reusability#reusing-logic

上一篇
Day 18: JavaScript 工廠函式 和 類別(class)
下一篇
Day 20: Vue - 依賴注入模式(provide/inject) 和 pinia 使用介紹
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言