Vue 3 与 Vant 集成指南
完整的 Vue 3 + Vant 开发指南,充分利用 Vue 3 的新特性构建现代化移动端应用。
🚀 Vue 3 新特性支持
Composition API 集成
Vant 4.x 完全支持 Vue 3 的 Composition API,让组件逻辑更加清晰和可复用。
基础用法示例
vue
<script setup>
import { ref, computed } from 'vue'
import { showToast, showDialog } from 'vant'
// 响应式数据
const count = ref(0)
const loading = ref(false)
const userInfo = ref({
name: '张三',
age: 25,
city: '北京'
})
// 计算属性
const displayInfo = computed(() => {
return `${userInfo.value.name} (${userInfo.value.age}岁) - ${userInfo.value.city}`
})
// 方法
const handleIncrement = async () => {
loading.value = true
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000))
count.value++
loading.value = false
showToast(`当前计数: ${count.value}`)
}
const handleReset = () => {
showDialog({
title: '确认重置',
message: '确定要重置计数器吗?',
}).then(() => {
count.value = 0
showToast('已重置')
}).catch(() => {
showToast('已取消')
})
}
</script>
<template>
<div class="demo-container">
<!-- 用户信息展示 -->
<van-cell-group title="用户信息">
<van-cell title="基本信息" :value="displayInfo" />
<van-cell title="当前计数" :value="count" />
</van-cell-group>
<!-- 操作按钮 -->
<div class="button-group">
<van-button
type="primary"
:loading="loading"
@click="handleIncrement"
block
>
增加计数
</van-button>
<van-button
type="danger"
:disabled="count === 0"
@click="handleReset"
block
>
重置计数
</van-button>
</div>
</div>
</template>
<style scoped>
.demo-container {
padding: 16px;
}
.button-group {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
响应式系统深度集成
使用 reactive 管理复杂状态
vue
<script setup>
import { reactive, computed, watch } from 'vue'
import { showNotify } from 'vant'
// 复杂状态管理
const state = reactive({
user: {
name: '',
email: '',
phone: ''
},
form: {
loading: false,
errors: {}
},
settings: {
notifications: true,
darkMode: false
}
})
// 表单验证
const isFormValid = computed(() => {
return state.user.name &&
state.user.email &&
state.user.phone
})
// 监听设置变化
watch(() => state.settings.darkMode, (newValue) => {
document.documentElement.classList.toggle('dark', newValue)
showNotify({
type: 'success',
message: `已切换到${newValue ? '深色' : '浅色'}模式`
})
})
// 表单提交
const handleSubmit = async () => {
if (!isFormValid.value) {
showNotify({ type: 'warning', message: '请填写完整信息' })
return
}
state.form.loading = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 2000))
showNotify({ type: 'success', message: '保存成功' })
} catch (error) {
showNotify({ type: 'danger', message: '保存失败' })
} finally {
state.form.loading = false
}
}
</script>
<template>
<div class="form-container">
<van-form @submit="handleSubmit">
<van-cell-group title="个人信息">
<van-field
v-model="state.user.name"
name="name"
label="姓名"
placeholder="请输入姓名"
:rules="[{ required: true, message: '请填写姓名' }]"
/>
<van-field
v-model="state.user.email"
name="email"
label="邮箱"
placeholder="请输入邮箱"
:rules="[
{ required: true, message: '请填写邮箱' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }
]"
/>
<van-field
v-model="state.user.phone"
name="phone"
label="手机号"
placeholder="请输入手机号"
:rules="[
{ required: true, message: '请填写手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
]"
/>
</van-cell-group>
<van-cell-group title="设置">
<van-cell title="通知推送">
<template #right-icon>
<van-switch v-model="state.settings.notifications" />
</template>
</van-cell>
<van-cell title="深色模式">
<template #right-icon>
<van-switch v-model="state.settings.darkMode" />
</template>
</van-cell>
</van-cell-group>
<div class="submit-section">
<van-button
type="primary"
native-type="submit"
:loading="state.form.loading"
:disabled="!isFormValid"
block
>
保存信息
</van-button>
</div>
</van-form>
</div>
</template>
<style scoped>
.form-container {
padding: 16px;
}
.submit-section {
margin-top: 24px;
}
</style>
🎯 TypeScript 完美支持
组件类型定义
typescript
// types/user.ts
export interface User {
id: number
name: string
email: string
avatar?: string
phone?: string
status: 'active' | 'inactive' | 'pending'
}
export interface UserFormData {
name: string
email: string
phone: string
}
// types/components.ts
export interface ButtonProps {
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default'
size?: 'large' | 'normal' | 'small' | 'mini'
loading?: boolean
disabled?: boolean
block?: boolean
}
TypeScript 组件示例
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User, UserFormData } from '@/types/user'
// Props 接口定义
interface Props {
user?: User
readonly?: boolean
showAvatar?: boolean
}
// 默认值设置
const props = withDefaults(defineProps<Props>(), {
readonly: false,
showAvatar: true
})
// Emits 类型定义
interface Emits {
(e: 'update', data: UserFormData): void
(e: 'delete', id: number): void
(e: 'save', user: User): void
}
const emit = defineEmits<Emits>()
// 响应式数据
const formData = ref<UserFormData>({
name: props.user?.name || '',
email: props.user?.email || '',
phone: props.user?.phone || ''
})
const loading = ref<boolean>(false)
// 计算属性
const isFormValid = computed((): boolean => {
return !!(formData.value.name &&
formData.value.email &&
formData.value.phone)
})
const avatarUrl = computed((): string => {
return props.user?.avatar || '/default-avatar.png'
})
// 方法
const handleSave = async (): Promise<void> => {
if (!isFormValid.value) return
loading.value = true
try {
const updatedUser: User = {
id: props.user?.id || Date.now(),
...formData.value,
status: 'active'
}
emit('save', updatedUser)
} finally {
loading.value = false
}
}
const handleDelete = (): void => {
if (props.user?.id) {
emit('delete', props.user.id)
}
}
// 暴露给父组件的方法
defineExpose({
validate: () => isFormValid.value,
reset: () => {
formData.value = {
name: '',
email: '',
phone: ''
}
}
})
</script>
<template>
<div class="user-form">
<!-- 头像显示 -->
<div v-if="showAvatar" class="avatar-section">
<van-image
:src="avatarUrl"
width="80"
height="80"
round
fit="cover"
/>
</div>
<!-- 表单内容 -->
<van-form @submit="handleSave">
<van-field
v-model="formData.name"
label="姓名"
placeholder="请输入姓名"
:readonly="readonly"
:rules="[{ required: true, message: '请填写姓名' }]"
/>
<van-field
v-model="formData.email"
label="邮箱"
placeholder="请输入邮箱"
:readonly="readonly"
:rules="[
{ required: true, message: '请填写邮箱' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }
]"
/>
<van-field
v-model="formData.phone"
label="手机号"
placeholder="请输入手机号"
:readonly="readonly"
:rules="[
{ required: true, message: '请填写手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
]"
/>
<!-- 操作按钮 -->
<div v-if="!readonly" class="action-buttons">
<van-button
type="primary"
:loading="loading"
:disabled="!isFormValid"
@click="handleSave"
block
>
保存
</van-button>
<van-button
v-if="user?.id"
type="danger"
plain
@click="handleDelete"
block
>
删除
</van-button>
</div>
</van-form>
</div>
</template>
<style scoped>
.user-form {
padding: 16px;
}
.avatar-section {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.action-buttons {
margin-top: 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
🔧 性能优化策略
按需引入配置
自动按需引入(推荐)
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
// 生成类型定义文件
dts: true,
// 自定义组件目录
dirs: ['src/components'],
// 包含的文件扩展名
extensions: ['vue', 'ts'],
// 深度搜索子目录
deep: true
})
],
// 构建优化
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'vant-vendor': ['vant'],
'utils-vendor': ['lodash-es', 'dayjs']
}
}
}
}
})
手动按需引入
javascript
// main.js
import { createApp } from 'vue'
import {
Button,
Cell,
CellGroup,
Field,
Form,
Toast,
Dialog,
Notify
} from 'vant'
// 引入对应样式
import 'vant/es/button/style'
import 'vant/es/cell/style'
import 'vant/es/cell-group/style'
import 'vant/es/field/style'
import 'vant/es/form/style'
import 'vant/es/toast/style'
import 'vant/es/dialog/style'
import 'vant/es/notify/style'
const app = createApp(App)
app.use(Button)
app.use(Cell)
app.use(CellGroup)
app.use(Field)
app.use(Form)
app.use(Toast)
app.use(Dialog)
app.use(Notify)
app.mount('#app')
组件懒加载
vue
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步组件
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
// 带加载状态的异步组件
const AsyncUserList = defineAsyncComponent({
loader: () => import('./components/UserList.vue'),
loadingComponent: () => h('van-loading', { type: 'spinner' }),
errorComponent: () => h('van-empty', { description: '加载失败' }),
delay: 200,
timeout: 3000
})
</script>
<template>
<div>
<Suspense>
<template #default>
<HeavyComponent />
<AsyncUserList />
</template>
<template #fallback>
<van-skeleton :row="3" />
</template>
</Suspense>
</div>
</template>
🎨 主题定制与样式
CSS 变量定制
css
/* 全局主题变量 */
:root {
/* 主色调 */
--van-primary-color: #1989fa;
--van-primary-color-dark: #0960bd;
--van-primary-color-light: #66b1ff;
/* 功能色 */
--van-success-color: #07c160;
--van-warning-color: #ff976a;
--van-danger-color: #ee0a24;
/* 文本颜色 */
--van-text-color: #323233;
--van-text-color-2: #646566;
--van-text-color-3: #969799;
/* 背景颜色 */
--van-background-color: #f7f8fa;
--van-background-color-light: #fafafa;
/* 边框 */
--van-border-color: #ebedf0;
--van-border-width: 1px;
/* 字体大小 */
--van-font-size-xs: 10px;
--van-font-size-sm: 12px;
--van-font-size-md: 14px;
--van-font-size-lg: 16px;
/* 间距 */
--van-padding-base: 4px;
--van-padding-xs: 8px;
--van-padding-sm: 12px;
--van-padding-md: 16px;
--van-padding-lg: 24px;
/* 圆角 */
--van-border-radius-sm: 2px;
--van-border-radius-md: 4px;
--van-border-radius-lg: 8px;
}
/* 深色模式 */
@media (prefers-color-scheme: dark) {
:root {
--van-primary-color: #4fc3f7;
--van-text-color: #ffffff;
--van-text-color-2: #cccccc;
--van-background-color: #1a1a1a;
--van-background-color-light: #2d2d2d;
--van-border-color: #333333;
}
}
组件级样式定制
vue
<script setup>
import { ref } from 'vue'
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', theme.value)
}
</script>
<template>
<div class="themed-container" :data-theme="theme">
<!-- 自定义按钮样式 -->
<van-button class="custom-button" type="primary">
自定义按钮
</van-button>
<!-- 自定义卡片样式 -->
<van-card class="custom-card" title="自定义卡片">
<template #footer>
<van-button size="small" @click="toggleTheme">
切换主题
</van-button>
</template>
</van-card>
</div>
</template>
<style scoped>
.themed-container {
padding: 16px;
}
/* 自定义按钮样式 */
.custom-button {
--van-button-primary-background-color: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--van-button-primary-border-color: transparent;
--van-button-border-radius: 20px;
font-weight: 600;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
}
.custom-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
/* 自定义卡片样式 */
.custom-card {
--van-card-background-color: var(--van-background-color-light);
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 深色主题适配 */
[data-theme="dark"] .custom-card {
--van-card-background-color: #2d2d2d;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
</style>
🚀 最佳实践总结
开发规范
组件设计原则
- 单一职责:每个组件只负责一个功能
- 可复用性:通过 props 和 slots 提供灵活性
- 类型安全:使用 TypeScript 确保类型安全
- 性能优化:合理使用缓存和懒加载
代码组织
- 使用 Composition API 组织逻辑
- 合理拆分 composables
- 统一的类型定义管理
- 清晰的文件夹结构
性能优化
- 按需引入组件和样式
- 使用异步组件和 Suspense
- 合理使用 v-memo 和 v-once
- 避免不必要的响应式数据
项目结构推荐
src/
├── components/ # 公共组件
│ ├── base/ # 基础组件
│ ├── business/ # 业务组件
│ └── layout/ # 布局组件
├── composables/ # 组合式函数
├── types/ # 类型定义
├── utils/ # 工具函数
├── styles/ # 样式文件
│ ├── variables.css # CSS 变量
│ ├── mixins.scss # Sass 混入
│ └── themes/ # 主题文件
├── views/ # 页面组件
└── router/ # 路由配置
📚 相关资源
💡 开发技巧
- 善用 Vue DevTools:调试 Composition API 和响应式数据
- 类型提示:充分利用 TypeScript 的智能提示
- 性能监控:使用 Vue DevTools 的性能面板
- 组件测试:编写单元测试确保组件质量
- 文档维护:保持组件文档的及时更新
通过以上指南,你可以充分发挥 Vue 3 和 Vant 的优势,构建高质量的移动端应用。