知识广场

按学科筛选:计算机科学 / 前端开发 / Vue
清除筛选

«计算机科学 / 前端开发 / Vue» 分类下共 1 篇帖子

Vue 3 Composition API:从 Options API 迁移的实际感受

## 起因 Vue 2 用 Options API 多年: ```vue <script> export default { data() { return { count: 0 }; }, computed: { doubled() { return this.count * 2; }, }, methods: { inc() { this.count++; }, }, mounted() { console.log('mounted'); }, }; </script> ``` 清晰简单。但复杂组件痛点: - 同 feature 的 data / method / watcher 分散在不同 section - 跨组件复用 logic 难(mixin 有命名冲突) - TS 类型推导弱 Vue 3 Composition API + `<script setup>`: ```vue <script setup> import { ref, computed, onMounted } from 'vue'; const count = ref(0); const doubled = computed(() => count.value * 2); function inc() { count.value++; } onMounted(() => console.log('mounted')); </script> ``` 更接近 React Hook 思路。重构 6 个月经验: ## 主要变化 ```vue <!-- Options API --> <template><button @click="inc">{{ count }}</button></template> <script> export default { data() { return { count: 0 }; }, methods: { inc() { this.count++; } }, }; </script> <!-- Composition API --> <template><button @click="inc">{{ count }}</button></template> <script setup> import { ref } from 'vue'; const count = ref(0); function inc() { count.value++; } </script> ``` - `data()` → `ref()` / `reactive()` - `methods` → 普通 function - `computed` → `computed()` - `watch` → `watch()` / `watchEffect()` - lifecycle → `onMounted` / `onUnmounted` 等 - props → `defineProps()` - emit → `defineEmits()` ## ref vs reactive ```ts const count = ref(0); // primitive count.value++; console.log(count.value); const state = reactive({ count: 0, name: '' }); // object state.count++; state.name = 'bob'; ``` `ref` 需 `.value` 访问(template 中自动 unwrap)。 `reactive` 用 Proxy 包对象,访问无 .value。 主流偏好:**全用 ref**(一致性,避免混乱)。 对 object 用 ref({ ... })。 ## composable (Hook 等价) 跨组件复用 logic: ```ts // composables/useCounter.ts import { ref } from 'vue'; export function useCounter(initial = 0) { const count = ref(initial); const inc = () => count.value++; const dec = () => count.value--; return { count, inc, dec }; } // 任意组件 <script setup> import { useCounter } from '@/composables/useCounter'; const { count, inc } = useCounter(); </script> ``` 跟 React custom hook 概念一致,命名 `useXxx`。 替代 Vue 2 mixin(没命名冲突 + 类型推导好)。 ## TS 类型自动 ```vue <script setup lang="ts"> const props = defineProps<{ title: string; count?: number; }>(); const emit = defineEmits<{ (e: 'update', value: number): void; }>(); // props.title typed string // emit('update', 42) typed </script> ``` vs Options API: ```vue <script> export default { props: { title: { type: String, required: true }, count: Number, }, emits: ['update'], }; </script> ``` TS-style 简洁 + 类型自动。 ## reactivity 注意 ```ts const state = ref({ count: 0 }); // ❌ destructure 失 reactive const { count } = state.value; count++; // 不更新 view // ✅ toRefs const stateRefs = toRefs(state.value); const { count } = stateRefs; count.value++; ``` destructure reactive object 是常见坑。解决用 `toRefs`。 ## watch vs watchEffect ```ts // watch(显式 source) watch(count, (newVal, oldVal) => { console.log(`count changed: ${oldVal} → ${newVal}`); }); // watchEffect(auto-track deps) watchEffect(() => { console.log(`count: ${count.value}`); }); ``` watch 像 React useEffect with explicit dep。 watchEffect 自动追踪用到的 ref。 ## lifecycle ```ts import { onMounted, onUnmounted, onUpdated, nextTick } from 'vue'; onMounted(async () => { await fetchData(); }); onUnmounted(() => { cleanup(); }); ``` 直观跟 Options API mapping。 ## provide / inject (Context) ```ts // 父 import { provide, ref } from 'vue'; const theme = ref('dark'); provide('theme', theme); // 子(任意深度) import { inject } from 'vue'; const theme = inject('theme'); ``` 跟 React Context 类似。typed key: ```ts import { InjectionKey } from 'vue'; const themeKey: InjectionKey<Ref<string>> = Symbol(); provide(themeKey, theme); const theme = inject(themeKey); // typed Ref<string> ``` ## 项目迁移经验 Vue 2 → Vue 3 大型项目: 1. Vue Compat(Vue 2/3 兼容层)渐进迁移 2. component 一个一个改 Composition API(同 file 内可以 Options 跟 Composition 共存暂时) 3. mixin → composable 4. global API(Vue.use → app.use) 我们 100 component 项目 1 个月迁完。 ROI: - 代码量降 20-30%(去掉 boilerplate) - 复杂组件可读性大幅提升 - TS 类型推导显著强 ## 仍能用 Options API Vue 3 兼容 Options API(permanent)。 没必要强迫迁。新 component 用 Composition,老的稳定就放。 ## 与 React Hook 对比 | | Vue Composition | React Hook | |---|---|---| | reactivity | Proxy (auto) | manual setState | | effect deps | 自动 track | 手动 dep array | | 模板 | SFC (`<template>`) | JSX | | 性能 | 默认细粒度 | 默认 re-render component | | 学习 | 中 | 中(dep array 陡) | Vue reactivity 更"魔法" + 不用记 dep。 React 更显式但 boilerplate 多。 ## script setup 是关键 只用 Composition API 但没 `<script setup>` → 仍要 `setup()` function + return。冗余。 `<script setup>`(SFC 顶部)让所有 top-level binding 自动可用 template。 减少 50%+ boilerplate。 强烈推荐每 Vue 3 component 都用。 ## 与 Nuxt 3 Nuxt 3 是 Vue 3 的 meta-framework(Next.js 类比)。 auto-import composable / page-based routing / SSR 等。 新 Vue 项目几乎都 Nuxt 3。 ## 真实 case:复杂 form 老 Vue 2 form 组件(800 行 Options API): - 30+ field,复杂校验 - 跨 step state - watcher 一堆 迁 Composition + Pinia + Zod: - 300 行(-60%) - 校验集中 schema 内 - step 切换 logic 用 composable 复用 - TS 类型保证 最大改进:思路清晰。一个 file 内"这 feature 的所有相关 code 在一起" (不是 data / methods / computed / watch 跳跃读)。 ## 踩过的坑 1. **forgot .value**:`if (count > 5)` 永远 truthy(ref 对象 truthy)。 `count.value > 5`。template 内不需要 .value 容易混。 2. **reactive deep limit**:reactive 是浅 → 嵌套 plain object 不 trigger update。`ref(deepObject)` 或者 `reactive` 递归。 3. **lifecycle order**:onMounted 内调子组件 ref → 子还没 mount。 await nextTick() 或 onMounted nested。 4. **props 解构丢 reactivity**:`const { title } = defineProps()` title 是 snapshot。`const props = defineProps(); props.title` 保 reactive。Vue 3.5+ 支持解构保留 reactivity。 5. **TS 推导慢**:复杂 generic component 编译变慢。简化 generic 或 拆分 component。