---
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
{{ item.name }}
```
### 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
]{{ count }}
```
### ❌ 不要在组件外使用 `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)