引言:解锁 Vue 3 的无限潜能
Vue 3 不仅带来了 Composition API 的革命性变化,还引入了许多高级特性和开发技巧。从 JSX/TSX 的灵活渲染到 defineModel
、defineSlots
等新特性,这些工具让我们能够构建更加强大、类型安全且易于维护的应用。
本文将通过实际案例,带你深入了解这些高级特性的使用方法和最佳实践。
1. JSX/TSX 在 Vue 3 中的应用
1.1 配置 JSX/TSX 支持
首先,我们需要配置项目以支持 JSX/TSX:
bash
# 安装必要的依赖
pnpm add @vitejs/plugin-vue-jsx
更新 vite.config.ts
:
ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx(), // 启用 JSX 支持
],
})
1.2 基础 JSX 语法
在 Vue 3 中,JSX 提供了更灵活的渲染方式:
tsx
// components/Button.tsx
import { defineComponent, type PropType } from 'vue'
interface ButtonProps {
type?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
onClick?: () => void
}
export default defineComponent({
name: 'Button',
props: {
type: {
type: String as PropType<ButtonProps['type']>,
default: 'primary'
},
size: {
type: String as PropType<ButtonProps['size']>,
default: 'md'
},
disabled: {
type: Boolean,
default: false
}
},
emits: ['click'],
setup(props, { emit, slots }) {
const handleClick = () => {
if (!props.disabled) {
emit('click')
}
}
return () => (
<button
class={[
'btn',
`btn-${props.type}`,
`btn-${props.size}`,
{ 'btn-disabled': props.disabled }
]}
disabled={props.disabled}
onClick={handleClick}
>
{slots.default?.()}
</button>
)
}
})
1.3 条件渲染与列表渲染
JSX 中的条件渲染和列表渲染更接近原生 JavaScript:
tsx
// components/TodoList.tsx
import { defineComponent, ref, computed } from 'vue'
interface Todo {
id: number
text: string
completed: boolean
}
export default defineComponent({
name: 'TodoList',
setup() {
const todos = ref<Todo[]>([
{ id: 1, text: '学习 Vue 3', completed: false },
{ id: 2, text: '掌握 JSX', completed: true },
{ id: 3, text: '构建项目', completed: false }
])
const newTodo = ref('')
const completedCount = computed(() =>
todos.value.filter(todo => todo.completed).length
)
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
}
const toggleTodo = (id: number) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
return () => (
<div class="todo-container">
<h2>Todo List ({completedCount.value}/{todos.value.length} 完成)</h2>
<div class="add-todo">
<input
v-model={newTodo.value}
placeholder="添加新任务..."
onKeypress={(e: KeyboardEvent) => {
if (e.key === 'Enter') addTodo()
}}
/>
<button onClick={addTodo}>添加</button>
</div>
<ul class="todo-list">
{todos.value.map(todo => (
<li
key={todo.id}
class={['todo-item', { completed: todo.completed }]}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span class="todo-text">{todo.text}</span>
{todo.completed && <span class="badge">✓</span>}
</li>
))}
</ul>
{todos.value.length === 0 && (
<div class="empty-state">
<p>暂无任务</p>
</div>
)}
</div>
)
}
})
2. defineModel - 双向绑定的新方式
defineModel
是 Vue 3.4+ 引入的新特性,简化了组件的双向绑定:
vue
<!-- components/CustomInput.vue -->
<script setup lang="ts">
// 传统方式需要 props + emit
// const props = defineProps<{ modelValue: string }>()
// const emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()
// 使用 defineModel 一行搞定
const model = defineModel<string>()
// 支持多个 model
const title = defineModel('title', { default: '' })
const content = defineModel('content', { default: '' })
// 支持修饰符
const lazy = defineModel('lazy', {
get(value: string) {
return value?.trim()
},
set(value: string) {
return value?.toLowerCase()
}
})
</script>
<template>
<div class="custom-input">
<label>基础输入:</label>
<input v-model="model" placeholder="输入内容..." />
<label>标题:</label>
<input v-model="title" placeholder="输入标题..." />
<label>内容:</label>
<textarea v-model="content" placeholder="输入内容..."></textarea>
<label>懒加载 (小写转换):</label>
<input v-model="lazy" placeholder="输入后失焦查看效果..." />
</div>
</template>
使用组件:
vue
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './components/CustomInput.vue'
const inputValue = ref('')
const articleTitle = ref('')
const articleContent = ref('')
const lazyValue = ref('')
</script>
<template>
<div>
<CustomInput
v-model="inputValue"
v-model:title="articleTitle"
v-model:content="articleContent"
v-model:lazy="lazyValue"
/>
<div class="preview">
<h3>实时预览:</h3>
<p>基础输入: {{ inputValue }}</p>
<p>标题: {{ articleTitle }}</p>
<p>内容: {{ articleContent }}</p>
<p>懒加载值: {{ lazyValue }}</p>
</div>
</div>
</template>
3. defineSlots - 类型安全的插槽
defineSlots
为插槽提供了完整的 TypeScript 支持:
vue
<!-- components/Card.vue -->
<script setup lang="ts">
interface CardSlots {
default?: (props: { data: any }) => any
header?: (props: { title: string }) => any
footer?: (props: { actions: string[] }) => any
empty?: () => any
}
const slots = defineSlots<CardSlots>()
defineProps<{
title?: string
data?: any[]
loading?: boolean
}>()
</script>
<template>
<div class="card">
<!-- 头部插槽 -->
<header v-if="slots.header" class="card-header">
<slot name="header" :title="title || '默认标题'" />
</header>
<!-- 主要内容 -->
<main class="card-content">
<div v-if="loading" class="loading">
加载中...
</div>
<div v-else-if="data && data.length > 0">
<slot :data="data" />
</div>
<div v-else class="empty">
<slot name="empty">
<p>暂无数据</p>
</slot>
</div>
</main>
<!-- 底部插槽 -->
<footer v-if="slots.footer" class="card-footer">
<slot name="footer" :actions="['编辑', '删除', '分享']" />
</footer>
</div>
</template>
使用带类型检查的插槽:
vue
<!-- UserList.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import Card from './components/Card.vue'
interface User {
id: number
name: string
email: string
avatar: string
}
const users = ref<User[]>([
{ id: 1, name: 'Alice', email: 'alice@example.com', avatar: '/avatar1.jpg' },
{ id: 2, name: 'Bob', email: 'bob@example.com', avatar: '/avatar2.jpg' }
])
const loading = ref(false)
</script>
<template>
<Card
title="用户列表"
:data="users"
:loading="loading"
>
<!-- 头部插槽 - 自动获得 title 参数的类型检查 -->
<template #header="{ title }">
<h2>{{ title }} ({{ users.length }} 位用户)</h2>
<button @click="loading = !loading">
{{ loading ? '停止' : '开始' }}加载
</button>
</template>
<!-- 默认插槽 - 自动获得 data 参数的类型检查 -->
<template #default="{ data }">
<div class="user-grid">
<div
v-for="user in data"
:key="user.id"
class="user-card"
>
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</div>
</template>
<!-- 空状态插槽 -->
<template #empty>
<div class="custom-empty">
<img src="/empty-users.svg" alt="无用户" />
<p>还没有用户数据</p>
<button>添加第一个用户</button>
</div>
</template>
<!-- 底部插槽 - 自动获得 actions 参数的类型检查 -->
<template #footer="{ actions }">
<div class="actions">
<button
v-for="action in actions"
:key="action"
class="action-btn"
>
{{ action }}
</button>
</div>
</template>
</Card>
</template>
4. 高级 Composition API 技巧
4.1 defineExpose - 组件方法暴露
vue
<!-- components/Modal.vue -->
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const isVisible = ref(false)
const modalRef = ref<HTMLElement>()
const open = async () => {
isVisible.value = true
await nextTick()
modalRef.value?.focus()
}
const close = () => {
isVisible.value = false
}
const toggle = () => {
isVisible.value ? close() : open()
}
// 暴露方法给父组件
defineExpose({
open,
close,
toggle,
isVisible: readonly(isVisible)
})
</script>
<template>
<Teleport to="body">
<div
v-if="isVisible"
ref="modalRef"
class="modal-overlay"
tabindex="-1"
@click.self="close"
@keydown.esc="close"
>
<div class="modal-content">
<slot />
<button @click="close">关闭</button>
</div>
</div>
</Teleport>
</template>
4.2 自定义 Composable 函数
ts
// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue'
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [Ref<T>, (value: T) => void, () => void] {
const storedValue = localStorage.getItem(key)
const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue
const state = ref<T>(initialValue)
const setValue = (value: T) => {
state.value = value
}
const removeValue = () => {
localStorage.removeItem(key)
state.value = defaultValue
}
// 监听变化并同步到 localStorage
watch(
state,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
},
{ deep: true }
)
return [state, setValue, removeValue]
}
ts
// composables/useAsyncData.ts
import { ref, computed, type Ref } from 'vue'
interface UseAsyncDataOptions<T> {
immediate?: boolean
onSuccess?: (data: T) => void
onError?: (error: Error) => void
}
export function useAsyncData<T>(
asyncFn: () => Promise<T>,
options: UseAsyncDataOptions<T> = {}
) {
const { immediate = true, onSuccess, onError } = options
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
const isReady = computed(() => !loading.value && data.value !== null)
const isError = computed(() => error.value !== null)
const execute = async () => {
try {
loading.value = true
error.value = null
const result = await asyncFn()
data.value = result
onSuccess?.(result)
} catch (err) {
error.value = err as Error
onError?.(err as Error)
} finally {
loading.value = false
}
}
const refresh = () => execute()
const reset = () => {
data.value = null
error.value = null
loading.value = false
}
if (immediate) {
execute()
}
return {
data: data as Ref<T | null>,
error,
loading,
isReady,
isError,
execute,
refresh,
reset
}
}
4.3 使用示例
vue
<!-- pages/UserProfile.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import Modal from '@/components/Modal.vue'
import { useLocalStorage } from '@/composables/useLocalStorage'
import { useAsyncData } from '@/composables/useAsyncData'
// 本地存储
const [userPreferences, setUserPreferences] = useLocalStorage('userPreferences', {
theme: 'light',
language: 'zh-CN'
})
// 异步数据获取
const { data: userProfile, loading, error, refresh } = useAsyncData(
() => fetch('/api/user/profile').then(res => res.json()),
{
onSuccess: (data) => console.log('用户数据加载成功:', data),
onError: (err) => console.error('加载失败:', err)
}
)
// 模态框引用
const modalRef = ref<InstanceType<typeof Modal>>()
const openEditModal = () => {
modalRef.value?.open()
}
const updateTheme = (theme: string) => {
setUserPreferences({ ...userPreferences.value, theme })
}
</script>
<template>
<div class="user-profile">
<div v-if="loading">加载中...</div>
<div v-else-if="error">加载失败: {{ error.message }}</div>
<div v-else-if="userProfile">
<h1>{{ userProfile.name }}</h1>
<p>{{ userProfile.email }}</p>
<div class="preferences">
<h3>偏好设置</h3>
<label>
主题:
<select :value="userPreferences.theme" @change="updateTheme($event.target.value)">
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</label>
</div>
<button @click="openEditModal">编辑资料</button>
<button @click="refresh">刷新</button>
</div>
<Modal ref="modalRef">
<h2>编辑用户资料</h2>
<form>
<!-- 表单内容 -->
</form>
</Modal>
</div>
</template>
5. 性能优化技巧
5.1 异步组件与懒加载
ts
// router/index.ts
import { defineAsyncComponent } from 'vue'
const router = createRouter({
routes: [
{
path: '/dashboard',
component: defineAsyncComponent({
loader: () => import('@/views/Dashboard.vue'),
loadingComponent: () => h('div', '加载中...'),
errorComponent: () => h('div', '加载失败'),
delay: 200,
timeout: 3000
})
}
]
})
5.2 KeepAlive 优化
vue
<!-- App.vue -->
<template>
<router-view v-slot="{ Component, route }">
<KeepAlive :include="['UserList', 'ProductList']">
<component
:is="Component"
:key="route.meta.keepAliveKey || route.fullPath"
/>
</KeepAlive>
</router-view>
</template>
6. 总结
Vue 3 的高级特性为我们提供了更强大、更灵活的开发方式:
- JSX/TSX 提供了类似 React 的渲染灵活性,同时保持 Vue 的响应式特性
- defineModel 简化了双向绑定的实现,减少了样板代码
- defineSlots 为插槽提供了完整的类型安全
- defineExpose 让组件方法暴露更加明确和可控
- 自定义 Composables 实现了逻辑复用的最佳实践
这些特性不仅提升了开发效率,还增强了代码的可维护性和类型安全性。在实际项目中,合理运用这些高级特性能够让我们构建出更加健壮和优雅的 Vue 应用。
掌握这些技巧,你将能够充分发挥 Vue 3 的强大潜能,构建出现代化的前端应用。