--- title: script setup Best Practices impact: HIGH impactDescription: 滥用 script setup 特性会导致类型安全问题、运行时错误以及更难维护的代码 type: best-practice tags: [vue3, composition-api, script-setup, typescript, best-practices] --- # script setup 最佳实践 **影响程度:高** — ` ``` **在真实项目中的重要性:** - Vue DevTools 显示组件名称而非 `` - `` 可以正常工作 - 递归组件可以引用自身 - 调试堆栈信息具有可读性 - 更易于在代码库中按组件名称搜索 ## TypeScript 集成 ### 带类型的 Props **BAD — 运行时声明:** ```vue ``` **GOOD — 基于类型的声明:** ```vue ``` ### 带类型的 Emits **BAD — 无类型安全:** ```vue ``` **GOOD — 类型安全的 emits:** ```vue ``` ## defineModel 实现双向绑定 **Vue 3.4+ 特性:** ```vue ``` **Vue 3.4 之前(手动实现):** ```vue ``` ## Store 访问模式 ### 在 Vue 组件内 — 使用 `useXxxStore()` ```vue ``` ### 在 Vue 组件外 — 使用 `useXxxStoreWithOut()` ```typescript // ✅ Good - 在 utils、hooks、plugins 中使用 WithOut 版本 import { useAppStoreWithOut } from '@/store/modules/app' export const useEngine = () => { const appStore = useAppStoreWithOut() // 传入全局 pinia 实例 // ... } ``` ```typescript // ✅ Good - 在 utils/migration.ts 中 import { useAppStoreWithOut } from '@/store/modules/app' export async function migrateOnlineIcons() { const appStore = useAppStoreWithOut() // 可以在组件上下文之外访问 store } ``` ```typescript // ✅ Good - 在 plugins/vue-i18n/index.ts 中 import { useLocaleStoreWithOut } from '@/store/modules/locale' export async function setupI18n(app: App) { const localeStore = useLocaleStoreWithOut() // ... } ``` **区分两者的原因:** - `useXxxStore()` 依赖 Vue 的 `inject`/`provide`,仅在组件上下文中可用 - `useXxxStoreWithOut(store)` 显式传入 pinia 实例,可在任意位置使用 - 使用错误会导致运行时错误:`getActivePinia was called with no active Pinia` 详见 [store-without-pattern](store-without-pattern.md)。 ## 常见模式 ### 带默认值的响应式 Props ```vue ``` ### 暴露组件方法 ```vue ``` ### 模板引用 ```vue ``` ### useTemplateRef(Vue 3.5+) **Vue 3.5 引入了 `useTemplateRef()`** — 一种类型安全的替代方案,用于替代普通的 `ref()` 来获取模板引用。它解决了尴尬的 `null` 初始化问题,并提供更好的类型推导: ```vue ``` **与 `ref` 的关键区别:** | 特性 | `ref` | `useTemplateRef()` | |---------|-------------------|-----------------------| | 初始值 | 必须指定 `null` | 自动推导,无需手动设置 null | | 挂载后的类型 | `T \| null` | `T \| undefined` | | 需要 ref 名称匹配 | 手动(靠约定) | 通过字符串参数强制匹配 | | 支持 v-for | ✅ `ref([])` | ✅ `useTemplateRef('list')` | ### useId(Vue 3.5+) **Vue 3.5 引入了 `useId()`**,用于生成唯一的、SSR 安全的 ID。对于无障碍访问(`aria-labelledby`、`for`/`id` 关联)和避免 ID 冲突至关重要: ```vue ``` **为什么不使用 `Math.random()` 或计数器?** - `useId()` 是 SSR 安全的 — 服务端和客户端生成匹配的 ID - 不会在组件实例之间产生冲突 - 在客户端导航(SPA 路由切换)之间会清除 ### Provide/Inject 与 TypeScript `provide`/`inject` 是避免 props 逐层传递的强大工具,但类型安全需要明确的模式: **步骤 1:定义 `InjectionKey`** ```typescript // types/injection-keys.ts import type { InjectionKey, Ref } from 'vue' // 类型化的 injection key export const THEME_KEY: InjectionKey> = Symbol('theme') export const CONFIG_KEY: InjectionKey = Symbol('config') ``` **步骤 2:带类型安全地 provide** ```vue ``` **步骤 3:带类型安全地 inject** ```vue ``` | 模式 | 返回类型 | 使用场景 | |---------|------------|-------------| | `inject(key)` 配合 `InjectionKey` | `T`(不可为 null) | 祖先组件中存在 provider | | `inject(key, default)` | `T`(不可为 null) | provider 可能不存在 | | `inject('key')` | `T \| undefined` | 旧的字符串 key 模式 | **⚠️ 在 TypeScript 中不要使用纯字符串进行 provide/inject** — 你会失去所有类型安全和 IDE 自动补全。 ### toValue() vs unref() **Vue 3.3 引入了 `toValue()`**,作为解包 "MaybeRef" 值的首选方式: ```typescript import { toValue, unref } from 'vue' import type { MaybeRef } from 'vue' // 两者都可以标准化 ref 和普通值 const a = ref(42) const b = 100 toValue(a) // 42 — 解包 ref,透传普通值 toValue(b) // 100 unref(a) // 42 — 行为相同 unref(b) // 100 // 但 toValue() 有一个关键区别: // toValue() 还可以解包 getter(返回值的函数) const getter = () => 42 toValue(getter) // 42 ✅ unref(getter) // () => 42 ❌(不会调用 getter) ``` **经验法则:** - 如果你的组合式函数接受 `MaybeRef` — 使用 `toValue()` 来标准化 - 如果你只处理 `Ref` 对象 — `unref()` 也可以 - 新代码应优先使用 `toValue()` 以保持向前兼容 ## 应避免的反模式 ### ❌ 不要与 Options API 混用 ```vue ``` ### ❌ 不要解构 Props ```vue ``` ### ❌ 不要在 script setup 中使用 `this` ```vue ``` ### ❌ 不要忘记 ref 的 `.value` ```vue ``` ### ❌ 不要在组件外使用 `useXxxStore()` ```typescript // ❌ Bad - 在 utils/hooks/plugins 中会抛出错误 import { useAppStore } from '@/store/modules/app' export function someUtil() { const store = useAppStore() // Error: pinia is not defined } // ✅ Good - 使用 WithOut 版本 import { useAppStoreWithOut } from '@/store/modules/app' export function someUtil() { const store = useAppStoreWithOut() // 正常工作! } ``` ## 性能提示 ### 对大型对象和动态组件使用 shallowRef ```vue ``` ### 对派生状态使用 computed ```vue ``` ## 参考资料 - [Vue.js script setup](https://vuejs.org/api/sfc-script-setup.html) - [Vue.js TypeScript with Composition API](https://vuejs.org/guide/typescript/composition-api.html) - [Vue.js defineModel](https://vuejs.org/api/sfc-script-setup.html#definemodel) - [Vue.js defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) - [Vue.js useTemplateRef](https://vuejs.org/api/composition-api-helpers.html#usetemplateref) - [Vue.js useId](https://vuejs.org/api/composition-api-helpers.html#useid) - [Vue.js provide/inject](https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity) - [Vue.js toValue](https://vuejs.org/api/reactivity-utilities.html#tovalue)