解構前端 Composable 架構:告別邏輯泥淖,打造高內聚的現代 UI 系統
在前端架構演進的歷史中,「如何優雅地複用邏輯」始終是核心命題。早年我們用過 Mixins,後來轉向高階組件(HOC)與 Render Props,但這幾種模式在規模化後往往演變成維護惡夢。
現代前端框架(Vue Composition API、React Hooks、SolidJS Primitives、Svelte Runes)逐步收斂出一個共通典範——Composable(可組合項)。
為什麼需要 Composable?痛點與演進
在 Options API 或傳統 Class 組件時期,組件通常按「語法類型」劃分區塊(data、methods、lifecycle hooks)。這種模式在小型功能尚可運作,但當業務邏輯膨脹時,會帶來三個致命痛點:
- 關注點碎片化(Fragmented Concerns):實現一個「即時資料輪詢」功能,狀態要寫在
data、啟動要寫在mounted、清理要寫在unmounted、觸發要寫在methods。單一功能的代碼被硬生生拆散在數百行代碼的各個角落。 - Mixins 的隱含衝突:Mixins 曾是邏輯複用的主力,但它帶來嚴重的「命名空間碰撞」與「黑盒依賴」——你很難一眼看出某個
this.userData究竟來自組件本身還是五個 Mixin 中的哪一個。 - 組件樹膨脹(Wrapper Hell):HOC 和 Render Props 雖然解決了命名衝突,但過度嵌套組件會增加渲染層級與除錯負擔。
Composable 的核心精神是:將有狀態的業務邏輯(Stateful Logic)封裝為獨立函式,以純粹的參數輸入與值輸出進行組裝。
實戰對比:Before vs. After
以常見的「視窗尺寸監聽(Window Resize)」為例。
Before:未抽離邏輯的傳統組件(以 Vue 2 / Options API 為例)
視窗監聽邏輯與組件的其他商業邏輯混雜,無法直接被其他組件複用:
export default {
data() {
return {
// 視窗狀態
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
// 其他業務狀態混雜在此
userList: [],
isLoading: false
}
},
mounted() {
// 監聽邏輯散落在生命週期
window.addEventListener('resize', this.onResize)
this.fetchUsers()
},
beforeDestroy() {
// 清理邏輯散落在另一處
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize() {
this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight
},
fetchUsers() { /* ... */ }
}
}
After:封裝為 Composable(以 Vue 3 Composition API 為例)
將狀態、事件監聽與銷毀清理完整封裝為 useWindowSize:
// composables/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const update = () => {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', update))
onUnmounted(() => window.removeEventListener('resize', update))
return { width, height }
}
在組件中只需一行呼叫,來源與型別完全透明:
<!-- components/ResponsiveDashboard.vue -->
<script setup>
import { useWindowSize } from '@/composables/useWindowSize'
// 邏輯即插即用,毫無侵入性
const { width, height } = useWindowSize()
</script>
<template>
<div>視窗解析度:{{ width }} x {{ height }}</div>
</template>
各主流框架的實現方式
雖然語法與響應式底層機制有所不同,但「函式化封裝狀態邏輯」的理念完全相通:
1. Vue 3 (Composition API)
- 核心機制:基於 Proxy 的依賴追蹤(
ref、reactive)。 - 特性:
setup()只執行一次建立響應依賴,後續狀態變更不重新執行整段函式,心智模型直觀。
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
return { count, doubleCount, increment }
}
2. React (Custom Hooks)
- 核心機制:基於 Fiber 渲染週期的 Hook 機制(
useState、useEffect)。 - 特性:狀態更新會觸發組件重新渲染,需注意
useCallback/useMemo與 Hook 調用順序限制(不可在條件式內調用)。
// useCounter.js
import { useState, useMemo, useCallback } from 'react'
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const doubleCount = useMemo(() => count * 2, [count])
const increment = useCallback(() => setCount(c => c + 1), [])
return { count, doubleCount, increment }
}
3. SolidJS (Primitives)
- 核心機制:細粒度響應系統(Signals)。
- 特性:組件函式本身只執行一次建立運算圖,更新時精確驅動 DOM 節點,無 Virtual DOM 開銷。
// createCounter.js
import { createSignal, createMemo } from 'solid-js'
export function createCounter(initialValue = 0) {
const [count, setCount] = createSignal(initialValue)
const doubleCount = createMemo(() => count() * 2)
const increment = () => setCount(c => c + 1)
return { count, doubleCount, increment }
}
4. Svelte 5 (Runes)
- 核心機制:編譯器驅動的訊號系統(
$state、$derived)。 - 特性:直接使用原生 JS 語法賦值,不再受限於
.svelte單檔案組件內,在標準.js/.ts模組中即可定義通用響應邏輯。
// counter.svelte.js
export function createCounter(initialValue = 0) {
let count = $state(initialValue)
let doubleCount = $derived(count * 2)
function increment() {
count += 1
}
return {
get count() { return count },
get doubleCount() { return doubleCount },
increment
}
}
架構設計原則
要在大型專案中寫好 Composable,建議遵循以下實務原則:
- 命名明確:遵循
useXxx(Vue/React)或createXxx(Solid)的約定,清楚表達功能意圖。 - 單一職責:一個 Composable 只專注解決一個問題(如
useAuth、usePagination、useClipboard)。 - 靈活組合(Composition):高階 Composable 應由基礎 Composable 拼裝而成。例如
useUserTable可內部調用useFetch與usePagination。 - 自動清理副作用:內部涉及事件監聽、計時器或 WebSocket 時,務必在內部生命週期或清理函式中完成釋放,避免調用方產生記憶體洩漏。