Agent skill

vue3-frontend-dev

Vue3 前端开发规范和最佳实践指南。适用于现代Vue3项目开发,涵盖代码组织、Composition API、状态管理、性能优化、测试等全方位内容。

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/vue3-frontend-dev

SKILL.md

Vue3 前端开发规范

技术栈

本项目基于 Vue 3 + TypeScript + Vite + Pinia + Vue Router 4 + Axios + Tailwind CSS + Element Plus/Vant

核心原则

  • 组件化设计:小而美、可复用、可组合
  • Composition API:使用 <script setup> 语法
  • 类型安全:强制使用TypeScript
  • 性能优先:代码分割、懒加载、虚拟滚动
  • 用户体验:响应式设计、加载状态、错误处理
  • 代码规范:ESLint + Prettier + Stylelint

项目结构

目录组织

project-root/
├── src/
│   ├── api/            # API接口定义
│   │   ├── user.ts
│   │   ├── product.ts
│   │   └── index.ts
│   ├── assets/         # 静态资源
│   │   ├── images/
│   │   ├── fonts/
│   │   └── styles/
│   ├── components/     # 公共组件
│   │   ├── common/     # 通用组件
│   │   └── business/   # 业务组件
│   ├── composables/    # 组合式函数
│   │   ├── useAuth.ts
│   │   ├── useRequest.ts
│   │   └── useTable.ts
│   ├── router/         # 路由配置
│   │   ├── index.ts
│   │   └── routes/
│   ├── stores/         # 状态管理(Pinia)
│   │   ├── user.ts
│   │   ├── app.ts
│   │   └── modules/
│   ├── types/          # TypeScript类型定义
│   │   ├── api.d.ts
│   │   ├── components.d.ts
│   │   └── global.d.ts
│   ├── utils/          # 工具函数
│   │   ├── request.ts  # Axios封装
│   │   ├── storage.ts  # 本地存储
│   │   └── validate.ts
│   ├── views/          # 页面组件
│   ├── App.vue
│   └── main.ts
├── public/             # 公共资源
├── tests/              # 测试文件
│   ├── unit/
│   └── e2e/
├── index.html
├── vite.config.ts
├── tsconfig.json
└── package.json

Vue3 规范

组件开发

基础组件结构

vue
<template>
  <div class="user-card">
    <h3 class="user-name">{{ user.name }}</h3>
    <p class="user-email">{{ user.email }}</p>
    <button @click="handleEdit">编辑</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { User } from '@/types/api'

// Props定义
interface Props {
  user: User
  readonly?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  readonly: false
})

// Emits定义
interface Emits {
  (e: 'edit', user: User): void
  (e: 'delete', userId: number): void
}

const emit = defineEmits<Emits>()

// 响应式状态
const isEditing = ref(false)
const formData = ref<User>({ ...props.user })

// 计算属性
const displayName = computed(() => {
  return props.user.name || '未知用户'
})

// 方法
const handleEdit = () => {
  emit('edit', props.user)
}

// 生命周期
onMounted(() => {
  console.log('UserCard mounted')
})
</script>

<style scoped lang="scss">
.user-card {
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  
  .user-name {
    font-size: 18px;
    font-weight: 600;
  }
  
  .user-email {
    color: #6b7280;
  }
}
</style>

命名规范

  • 组件文件:PascalCase(如 UserCard.vue
  • 变量和函数:camelCase(如 userNamehandleClick
  • 组件标签:kebab-case(如 <user-card>
  • Props:camelCase(如 userName
  • Events:kebab-case(如 @user-update
vue
<!-- 组件文件名:UserCard.vue -->
<template>
  <div>
    <user-card :user="userData" @user-update="handleUpdate" />
  </div>
</template>

<script setup lang="ts">
const userData = ref<User>({ name: '张三', email: 'zhangsan@example.com' })
const handleUpdate = (updatedUser: User) => {
  console.log('用户更新:', updatedUser)
}
</script>

Composition API 最佳实践

组合式函数

typescript
// composables/useAuth.ts
import { ref, computed } from 'vue'
import { useUserStore } from '@/stores/user'

export function useAuth() {
  const userStore = useUserStore()
  const isAuthenticated = computed(() => userStore.isAuthenticated)
  const user = computed(() => userStore.user)
  
  const login = async (credentials: LoginRequest) => {
    try {
      await userStore.login(credentials)
      return true
    } catch (error) {
      console.error('登录失败:', error)
      return false
    }
  }
  
  const logout = async () => {
    await userStore.logout()
  }
  
  return {
    isAuthenticated,
    user,
    login,
    logout
  }
}

使用组合式函数

vue
<script setup lang="ts">
import { useAuth } from '@/composables/useAuth'

const { isAuthenticated, user, login, logout } = useAuth()

const handleLogin = async () => {
  const success = await login({
    username: 'admin',
    password: 'password123'
  })
  if (success) {
    console.log('登录成功')
  }
}
</script>

API 调用规范

Axios 封装

typescript
// utils/request.ts
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import type { Result } from '@/types/api'

// 创建axios实例
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    // 添加token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse<Result>) => {
    const { code, data, message } = response.data
    
    // 成功响应
    if (code === 0) {
      return data
    }
    
    // 业务错误
    ElMessage.error(message || '请求失败')
    return Promise.reject(new Error(message))
  },
  (error) => {
    // HTTP错误
    if (error.response) {
      const { status } = error.response
      switch (status) {
        case 401:
          ElMessage.error('未授权,请重新登录')
          // 跳转登录页
          window.location.href = '/login'
          break
        case 403:
          ElMessage.error('拒绝访问')
          break
        case 404:
          ElMessage.error('请求的资源不存在')
          break
        case 500:
          ElMessage.error('服务器错误')
          break
        default:
          ElMessage.error(`请求失败: ${status}`)
      }
    } else {
      ElMessage.error('网络错误,请检查网络连接')
    }
    
    return Promise.reject(error)
  }
)

export default service

API 接口定义

typescript
// api/user.ts
import request from '@/utils/request'
import type { User, UserCreate, UserUpdate, UserListParams } from '@/types/api'

/**
 * 获取用户列表
 */
export function getUserList(params: UserListParams) {
  return request<User[]>({
    url: '/api/v1/users',
    method: 'get',
    params
  })
}

/**
 * 获取用户详情
 */
export function getUserById(id: number) {
  return request<User>({
    url: `/api/v1/users/${id}`,
    method: 'get'
  })
}

/**
 * 创建用户
 */
export function createUser(data: UserCreate) {
  return request<User>({
    url: '/api/v1/users',
    method: 'post',
    data
  })
}

/**
 * 更新用户
 */
export function updateUser(id: number, data: UserUpdate) {
  return request<User>({
    url: `/api/v1/users/${id}`,
    method: 'put',
    data
  })
}

/**
 * 删除用户
 */
export function deleteUser(id: number) {
  return request<void>({
    url: `/api/v1/users/${id}`,
    method: 'delete'
  })
}

使用Mock数据

typescript
// api/mock.ts
export const MOCK_ENABLED = import.meta.env.VITE_MOCK_ENABLED === 'true'

export const mockUserList: User[] = [
  {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com'
  },
  {
    id: 2,
    name: '李四',
    email: 'lisi@example.com'
  }
]

// api/user.ts
import { MOCK_ENABLED, mockUserList } from './mock'

export function getUserList(params: UserListParams) {
  if (MOCK_ENABLED) {
    return Promise.resolve(mockUserList)
  }
  
  return request<User[]>({
    url: '/api/v1/users',
    method: 'get',
    params
  })
}

状态管理

Pinia Store 定义

typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types/api'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string>('')
  const permissions = ref<string[]>([])
  
  // Getters
  const isAuthenticated = computed(() => !!token.value)
  const userName = computed(() => user.value?.name || '')
  const hasPermission = computed(() => {
    return (permission: string) => permissions.value.includes(permission)
  })
  
  // Actions
  const setUser = (userData: User) => {
    user.value = userData
  }
  
  const setToken = (tokenValue: string) => {
    token.value = tokenValue
    localStorage.setItem('token', tokenValue)
  }
  
  const setPermissions = (perms: string[]) => {
    permissions.value = perms
  }
  
  const login = async (credentials: LoginRequest) => {
    // 调用API登录
    const response = await loginApi(credentials)
    setUser(response.user)
    setToken(response.token)
    setPermissions(response.permissions)
  }
  
  const logout = async () => {
    // 调用API登出
    await logoutApi()
    user.value = null
    token.value = ''
    permissions.value = []
    localStorage.removeItem('token')
  }
  
  const updateUserInfo = (updates: Partial<User>) => {
    if (user.value) {
      Object.assign(user.value, updates)
    }
  }
  
  return {
    user,
    token,
    permissions,
    isAuthenticated,
    userName,
    hasPermission,
    setUser,
    setToken,
    setPermissions,
    login,
    logout,
    updateUserInfo
  }
})

使用Store

vue
<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 访问state
console.log(userStore.user)

// 访问getters
console.log(userStore.isAuthenticated)
console.log(userStore.userName)

// 调用actions
const handleLogin = async () => {
  await userStore.login({
    username: 'admin',
    password: 'password123'
  })
}

const handleLogout = async () => {
  await userStore.logout()
}
</script>

<template>
  <div v-if="userStore.isAuthenticated">
    欢迎您,{{ userStore.userName }}
  </div>
  <div v-else>
    请先登录
  </div>
</template>

路由配置

路由定义

typescript
// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/Users.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    // 需要认证但未登录,跳转到登录页
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

export default router

TypeScript 类型定义

API 类型

typescript
// types/api.d.ts

// 通用响应
export interface Result<T = any> {
  code: number
  data: T
  message: string
}

// 分页响应
export interface PageResult<T> {
  list: T[]
  total: number
  page: number
  pageSize: number
}

// 用户相关
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  status: number
  createdAt: string
  updatedAt: string
}

export interface UserCreate {
  name: string
  email: string
  password: string
}

export interface UserUpdate {
  name?: string
  email?: string
  avatar?: string
  status?: number
}

export interface UserListParams {
  page?: number
  pageSize?: number
  name?: string
  email?: string
  status?: number
}

// 登录请求
export interface LoginRequest {
  username: string
  password: string
}

// 登录响应
export interface LoginResponse {
  user: User
  token: string
  permissions: string[]
}

组件类型

typescript
// types/components.d.ts
export interface BaseProps {
  id?: string | number
  class?: string
  style?: string | Record<string, any>
}

export interface ButtonProps extends BaseProps {
  type?: 'primary' | 'success' | 'warning' | 'danger'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  loading?: boolean
}

export interface FormProps extends BaseProps {
  model: Record<string, any>
  rules?: Record<string, any>
  labelWidth?: string | number
}

性能优化

代码分割和懒加载

typescript
// 路由级别代码分割
const routes: RouteRecordRaw[] = [
  {
    path: '/users',
    component: () => import('@/views/Users.vue')  // 懒加载
  }
]

// 组件级别懒加载
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

虚拟滚动

vue
<template>
  <div class="virtual-list">
    <virtual-list
      :data-sources="items"
      :data-key="'id'"
      :keeps="30"
      :estimate-size="50"
    >
      <template #default="{ source }">
        <div class="list-item">{{ source.name }}</div>
      </template>
    </virtual-list>
  </div>
</template>

防抖和节流

typescript
// composables/useDebounce.ts
import { ref, watch } from 'vue'

export function useDebounce<T>(value: Ref<T>, delay: number = 300) {
  const debouncedValue = ref<T>(value.value)
  
  let timeout: ReturnType<typeof setTimeout>
  
  watch(value, (newValue) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })
  
  return debouncedValue
}

测试规范

单元测试

typescript
// tests/unit/components/UserCard.spec.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import UserCard from '@/components/UserCard.vue'

describe('UserCard.vue', () => {
  const mockUser = {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com'
  }
  
  it('渲染用户信息', () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser
      }
    })
    
    expect(wrapper.find('.user-name').text()).toBe('张三')
    expect(wrapper.find('.user-email').text()).toBe('zhangsan@example.com')
  })
  
  it('触发编辑事件', async () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser
      }
    })
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted('edit')).toBeTruthy()
    expect(wrapper.emitted('edit')?.[0]).toEqual([mockUser])
  })
})

严格禁止事项

  1. 直接修改Props:禁止直接修改props,使用emit通知父组件
  2. v-if和v-show混用:避免混用,根据场景选择
  3. 全局变量:避免使用全局变量,使用Pinia管理状态
  4. 内联样式过多:避免大量内联样式,使用Scoped CSS
  5. 过度使用watch:优先使用computed
  6. 空函数:禁止生成空函数或使用测试代码

最佳实践总结

关注点分离

  • 组件:UI展示和用户交互
  • Composables:可复用的逻辑
  • Stores:全局状态管理
  • API:数据请求

可维护性

  • TypeScript类型安全
  • 代码复用(组件、composables)
  • 统一的代码规范
  • 完善的注释文档

性能优化

  • 代码分割和懒加载
  • 虚拟滚动
  • 防抖节流
  • 合理使用缓存

Didn't find tool you were looking for?

Be as detailed as possible for better results