Appearance
第 19 章 设计模式与架构决策
本章要点
- 组合式函数(Composables)的设计原则:单一职责、参数归一化、返回值契约
- Composables 如何利用 Vue 的响应式系统实现逻辑复用,以及与 Mixins/HOC 的本质区别
- Renderless Components 与 Headless UI:将行为逻辑与视觉呈现彻底分离
- 状态机模式:用有限状态机(FSM)管理复杂交互,杜绝"状态爆炸"问题
- 依赖注入(provide/inject)、Props Drilling、全局 Store 三种通信模式的适用场景与代价
- 微前端架构中 Vue 应用的隔离策略:样式隔离、状态隔离、路由协调
- Feature-Sliced Design:大型项目的模块化分层架构实践
- 从源码和运行时角度理解每种模式的性能特征与限制
框架提供了原语(primitive),但架构决定了应用的上限。Vue 3 的 Composition API、响应式系统、编译器优化为开发者提供了强大的底层能力,但如何将这些能力组织成可维护、可扩展的应用架构,是每个团队都必须面对的问题。
前 18 章我们深入剖析了 Vue 3 的内核实现——从响应式系统的依赖追踪,到编译器的静态优化,再到组件系统的生命周期调度。本章将视角从"框架如何工作"转向"如何用好框架",探讨 Vue 生态中最重要的设计模式与架构决策。这些模式不是凭空而来的最佳实践,每一个都有其在 Vue 运行时中的实现基础和性能特征。
19.1 组合式函数(Composables):逻辑复用的最佳范式
从 Mixins 到 Composables 的进化
Vue 2 时代的 Mixins 是逻辑复用的主要手段,但它有三个致命缺陷:命名冲突(多个 Mixin 可能定义同名属性)、来源不透明(模板中使用的变量不知道来自哪个 Mixin)、隐式耦合(Mixin 之间可能互相依赖)。Composition API 的设计正是为了彻底解决这些问题。
typescript
// ❌ Vue 2 Mixins:命名冲突 + 来源不透明
const mouseTracker = {
data() {
return { x: 0, y: 0 } // 与其他 Mixin 冲突?
},
mounted() {
window.addEventListener('mousemove', this.update)
},
methods: {
update(e: MouseEvent) {
this.x = e.pageX
this.y = e.pageY
}
}
}
// ✅ Vue 3 Composables:显式导入 + 可溯源
import { ref, onMounted, onUnmounted } from 'vue'
import type { Ref } from 'vue'
interface UseMouseReturn {
x: Ref<number>
y: Ref<number>
}
export function useMouse(): UseMouseReturn {
const x = ref(0)
const y = ref(0)
function update(e: MouseEvent) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}Composables 之所以能解决 Mixins 的全部问题,关键在于它利用了 JavaScript 的词法作用域——每次调用 useMouse() 都会创建独立的闭包,状态天然隔离。同时,返回值是显式的,使用方清楚知道每个变量的来源。
Composables 的运行时机制
Composables 并不是 Vue 运行时的特殊构造,它本质上就是一个普通函数。它之所以能"绑定"到组件的生命周期,靠的是 Vue 内部的 currentInstance 机制:
typescript
// runtime-core/src/component.ts
// Vue 维护一个全局变量记录当前正在初始化的组件实例
export let currentInstance: ComponentInternalInstance | null = null
export function setCurrentInstance(instance: ComponentInternalInstance) {
currentInstance = instance
}
// runtime-core/src/apiLifecycle.ts
// 生命周期钩子通过 currentInstance 绑定到当前组件
export function onMounted(hook: () => void) {
if (currentInstance) {
// 将钩子注册到当前实例的生命周期队列
;(currentInstance.m || (currentInstance.m = [])).push(hook)
} else if (__DEV__) {
warn('onMounted is called when there is no active component instance.')
}
}这就是为什么 Composables 必须在 setup() 函数的同步执行阶段调用——因为只有在这个阶段,currentInstance 才指向正确的组件实例。如果在异步回调中调用 onMounted,currentInstance 可能已经指向另一个组件甚至为 null。
typescript
// ❌ 错误:异步调用 Composable
async function setup() {
await someAsyncOperation()
// 此时 currentInstance 可能已经丢失
const { x, y } = useMouse() // 生命周期钩子无法正确绑定
}
// ✅ 正确:同步调用,异步操作放在内部
function setup() {
const { x, y } = useMouse() // 同步调用,currentInstance 有效
const data = useAsyncData('/api/data') // 异步操作在 Composable 内部处理
return { x, y, data }
}高质量 Composable 的设计原则
经过大量实践,社区总结出一套 Composable 的设计契约:
typescript
// 原则 1:参数归一化(MaybeRef 模式)
import { ref, watch, unref, type MaybeRef } from 'vue'
export function useTitle(title: MaybeRef<string>) {
// 无论传入 string 还是 Ref<string>,都统一处理
watch(
() => unref(title),
(newTitle) => {
document.title = newTitle
},
{ immediate: true }
)
}
// 使用方式灵活
useTitle('静态标题')
useTitle(ref('动态标题'))
useTitle(computed(() => `${page.value} - My App`))typescript
// 原则 2:返回值使用 ref 而非 reactive
// 这样使用方可以解构而不丢失响应性
export function useFetch<T>(url: MaybeRef<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const response = await fetch(unref(url))
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
// 当 url 是 ref 时,自动重新请求
watch(() => unref(url), execute, { immediate: true })
return { data, error, loading, execute }
}
// ✅ 解构后仍然保持响应性
const { data, loading } = useFetch<User[]>('/api/users')typescript
// 原则 3:副作用自清理
export function useEventListener<K extends keyof WindowEventMap>(
target: Window | HTMLElement,
event: K,
handler: (e: WindowEventMap[K]) => void,
options?: AddEventListenerOptions
) {
onMounted(() => target.addEventListener(event, handler as EventListener, options))
// 组件卸载时自动移除监听器,调用方无需手动清理
onUnmounted(() => target.removeEventListener(event, handler as EventListener, options))
}