Skip to content

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>

🚀 最佳实践总结

开发规范

  1. 组件设计原则

    • 单一职责:每个组件只负责一个功能
    • 可复用性:通过 props 和 slots 提供灵活性
    • 类型安全:使用 TypeScript 确保类型安全
    • 性能优化:合理使用缓存和懒加载
  2. 代码组织

    • 使用 Composition API 组织逻辑
    • 合理拆分 composables
    • 统一的类型定义管理
    • 清晰的文件夹结构
  3. 性能优化

    • 按需引入组件和样式
    • 使用异步组件和 Suspense
    • 合理使用 v-memo 和 v-once
    • 避免不必要的响应式数据

项目结构推荐

src/
├── components/          # 公共组件
│   ├── base/           # 基础组件
│   ├── business/       # 业务组件
│   └── layout/         # 布局组件
├── composables/        # 组合式函数
├── types/              # 类型定义
├── utils/              # 工具函数
├── styles/             # 样式文件
│   ├── variables.css   # CSS 变量
│   ├── mixins.scss     # Sass 混入
│   └── themes/         # 主题文件
├── views/              # 页面组件
└── router/             # 路由配置

📚 相关资源

💡 开发技巧

  1. 善用 Vue DevTools:调试 Composition API 和响应式数据
  2. 类型提示:充分利用 TypeScript 的智能提示
  3. 性能监控:使用 Vue DevTools 的性能面板
  4. 组件测试:编写单元测试确保组件质量
  5. 文档维护:保持组件文档的及时更新

通过以上指南,你可以充分发挥 Vue 3 和 Vant 的优势,构建高质量的移动端应用。

基于Vant构建的企业级移动端解决方案