ts+vue3开发规范

36 阅读2分钟

针对 Vue 3 项目,TypeScript 的最佳实践会结合 Vue 的 Composition API 和响应式系统的特点,形成一套独特的规范体系。以下是专门为 Vue 3 + TypeScript 项目整理的实践指南:

🎯 项目初始化与配置

使用官方脚手架创建项目

npm create vue@latest
# 或
npm create vite@latest

在创建时务必勾选 TypeScript 选项,确保获得最佳的项目模板和配置。

环境类型声明

创建 src/env.d.ts 或扩充现有声明文件:

/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

// 环境变量类型声明
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_BASE_URL: string
  // 更多环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

📦 组件开发规范

1. 使用 <script setup> 语法

这是 Vue 3 推荐的组合式 API 写法,配合 TypeScript 体验最佳:

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// 通过泛型定义 ref 类型
const count = ref<number>(0)
const user = ref<User | null>(null)

// 类型安全的计算属性
const doubleCount = computed<number>(() => count.value * 2)

// 事件声明
const emit = defineEmits<{
  (e: 'change', value: number): void
  (e: 'update', id: string): void
}>()

// Props 定义 - 使用类型字面量
interface Props {
  title: string
  items?: string[]
  disabled?: boolean
  config?: {
    theme: 'light' | 'dark'
    size: 'small' | 'medium' | 'large'
  }
}

const props = withDefaults(defineProps<Props>(), {
  items: () => [],
  disabled: false,
  config: () => ({ theme: 'light', size: 'medium' })
})

// 暴露给父组件的属性和方法
defineExpose<{
  reset: () => void
  validate: () => boolean
}>({
  reset: () => { count.value = 0 },
  validate: () => count.value > 0
})
</script>

2. 组件 Props 的严格定义

为组件 Props 定义精确的类型,避免使用过于宽泛的类型:

// ✅ 好的做法
interface TableColumn {
  key: string
  title: string
  width?: number | string
  align?: 'left' | 'center' | 'right'
  sortable?: boolean
}

// ❌ 避免
interface TableColumn {
  key: string
  title: string
  width?: any
  align?: string
}

3. 模板引用的类型安全

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// 定义组件实例类型
const modalRef = ref<InstanceType<typeof ModalComponent> | null>(null)

// 或使用组件导出的类型
import ModalComponent, { type ModalExpose } from './ModalComponent.vue'
const modalRef = ref<ModalExpose | null>(null)

onMounted(() => {
  modalRef.value?.open() // 类型安全的方法调用
})
</script>

<template>
  <ModalComponent ref="modalRef" />
</template>

🔄 Composition API 最佳实践

1. 创建类型安全的组合式函数

// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'

interface AsyncState<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  execute: () => Promise<void>
}

export function useAsyncData<T>(
  fetcher: () => Promise<T>
): AsyncState<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const loading = ref(false)
  const error = ref<Error | null>(null)

  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  return {
    data,
    loading,
    error,
    execute
  }
}

// 使用示例
interface User {
  id: number
  name: string
  email: string
}

const { data, loading, error } = useAsyncData<User>(
  () => fetch('/api/user').then(res => res.json())
)

2. 类型安全的 Pinia Store

// stores/user.ts
import { defineStore } from 'pinia'

interface UserState {
  profile: User | null
  permissions: string[]
  lastLogin: Date | null
}

interface UserActions {
  login: (credentials: Credentials) => Promise<void>
  logout: () => void
  updateProfile: (data: Partial<User>) => Promise<User>
}

interface UserGetters {
  isAdmin: (state: UserState) => boolean
  fullName: (state: UserState) => string
}

export const useUserStore = defineStore<'user', UserState, UserGetters, UserActions>('user', {
  state: (): UserState => ({
    profile: null,
    permissions: [],
    lastLogin: null
  }),
  
  getters: {
    isAdmin: (state) => state.permissions.includes('admin'),
    fullName: (state) => state.profile ? 
      `${state.profile.firstName} ${state.profile.lastName}` : ''
  },
  
  actions: {
    async login(credentials: Credentials) {
      // 类型安全的登录逻辑
    },
    
    logout() {
      this.$patch({
        profile: null,
        permissions: [],
        lastLogin: null
      })
    },
    
    async updateProfile(data: Partial<User>) {
      // 部分更新,类型安全
    }
  }
})

// 在组件中使用
const userStore = useUserStore()
const { profile, isAdmin } = storeToRefs(userStore)

🛣️ Vue Router 类型安全

1. 类型化路由参数

// router/types.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    roles?: Array<'admin' | 'user'>
    title?: string
    transition?: string
    keepAlive?: boolean
  }
}

// router/index.ts
export const routes = [
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    meta: {
      requiresAuth: true,
      title: '用户详情',
      roles: ['admin']
    } as const // 使用 const assertion 确保类型精确
  }
] as const // 使路由配置成为字面量类型

2. 路由参数的类型推导

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 使用类型守卫处理路由参数
const userId = computed(() => {
  const id = route.params.id
  // 路由参数可能是 string 或 string[]
  return Array.isArray(id) ? id[0] : id
})

// 类型安全的导航
function navigateToUser(id: number) {
  router.push({
    name: 'UserDetail',
    params: { id: id.toString() }
  })
}

// 使用查询参数
const page = computed(() => {
  const page = route.query.page
  return page ? Number(page) : 1
})
</script>

📡 API 层类型安全

1. 统一的 API 响应类型

// api/types.ts
export interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
  timestamp: number
}

export interface PaginatedResponse<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
  totalPages: number
}

export interface ApiError {
  code: number
  message: string
  details?: Record<string, any>
}

2. 类型安全的请求函数

// api/user.ts
import axios, { type AxiosRequestConfig } from 'axios'
import type { ApiResponse, PaginatedResponse } from './types'

export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  role: 'admin' | 'editor' | 'viewer'
  createdAt: string
}

export interface CreateUserDto {
  name: string
  email: string
  password: string
  role?: User['role']
}

export interface UpdateUserDto extends Partial<CreateUserDto> {
  id: number
}

export class UserApi {
  private baseUrl = '/api/users'
  
  // 泛型方法,返回类型安全的数据
  async getUsers(params?: { page?: number; pageSize?: number }): Promise<PaginatedResponse<User>> {
    const response = await axios.get<ApiResponse<PaginatedResponse<User>>>(this.baseUrl, {
      params
    })
    return response.data.data
  }
  
  async getUserById(id: number): Promise<User> {
    const response = await axios.get<ApiResponse<User>>(`${this.baseUrl}/${id}`)
    return response.data.data
  }
  
  async createUser(data: CreateUserDto): Promise<User> {
    const response = await axios.post<ApiResponse<User>>(this.baseUrl, data)
    return response.data.data
  }
  
  async updateUser(data: UpdateUserDto): Promise<User> {
    const { id, ...rest } = data
    const response = await axios.put<ApiResponse<User>>(`${this.baseUrl}/${id}`, rest)
    return response.data.data
  }
  
  async deleteUser(id: number): Promise<void> {
    await axios.delete(`${this.baseUrl}/${id}`)
  }
}

export const userApi = new UserApi()

🎨 样式与 CSS Modules

类型安全的 CSS Modules

<script setup lang="ts">
// 为 CSS Modules 生成类型
import styles from './Component.module.css'

// styles 有完整的类型提示
// styles.container, styles.title, styles.highlight
</script>

<template>
  <div :class="styles.container">
    <h1 :class="styles.title">标题</h1>
  </div>
</template>

<style module="styles">
.container { /* ... */ }
.title { /* ... */ }
</style>

🔍 类型检查与工具

1. 使用 Volar 替代 Vetur

在 VS Code 中,为 Vue 3 项目推荐使用 Volar 扩展,并启用 Takeover Mode 以获得最佳的类型支持。

.vscode/settings.json 中配置:

{
  "typescript.tsdk": "node_modules/typescript/lib",
  "volar.takeoverMode.enabled": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

2. 在 package.json 中添加类型检查脚本

{
  "scripts": {
    "type-check": "vue-tsc --noEmit",
    "type-check:watch": "vue-tsc --noEmit --watch",
    "build": "vue-tsc && vite build"
  }
}

📝 ESLint 配置示例

// .eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier'
  ],
  rules: {
    // Vue 特定规则
    'vue/multi-word-component-names': 'error',
    'vue/component-name-in-template-casing': ['error', 'PascalCase'],
    'vue/define-props-declaration': ['error', 'type-based'],
    'vue/define-emits-declaration': ['error', 'type-based'],
    
    // TypeScript 规则
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { 
      argsIgnorePattern: '^_',
      varsIgnorePattern: '^_' 
    }],
    
    // 禁止使用 console.log,除非是 warn/error
    'no-console': ['error', { allow: ['warn', 'error'] }],
  }
}

💡 项目目录结构建议

src/
├── assets/            # 静态资源
├── components/        # 公共组件
│   ├── common/       # 基础组件
│   └── layout/       # 布局组件
├── composables/       # 组合式函数
│   ├── useAuth.ts
│   ├── useAsyncData.ts
│   └── index.ts      # 统一导出
├── views/            # 页面组件
│   ├── Home/
│   │   ├── index.vue
│   │   ├── components/  # 页面专属组件
│   │   └── types.ts     # 页面专属类型
│   └── User/
├── router/           # 路由配置
│   ├── index.ts
│   ├── routes.ts
│   └── guards.ts
├── stores/           # Pinia 状态管理
│   ├── user.ts
│   └── app.ts
├── api/              # API 请求
│   ├── client.ts     # axios 实例配置
│   ├── types.ts      # API 公共类型
│   └── modules/      # 按模块划分
│       ├── user.ts
│       └── product.ts
├── types/            # 全局类型定义
│   ├── global.d.ts
│   ├── env.d.ts
│   └── models/       # 领域模型类型
├── utils/            # 工具函数
│   ├── format.ts
│   └── validation.ts
├── styles/           # 全局样式
└── main.ts           # 入口文件

🔧 进阶类型技巧

1. 为 provide/inject 提供类型

// types/context.ts
import type { InjectionKey, Ref } from 'vue'

export interface ThemeContext {
  theme: Ref<'light' | 'dark'>
  toggleTheme: () => void
}

export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')

// 父组件提供
const theme = ref<'light' | 'dark'>('light')
provide(themeKey, {
  theme,
  toggleTheme: () => theme.value = theme.value === 'light' ? 'dark' : 'light'
})

// 子组件注入
const themeContext = inject(themeKey)
if (themeContext) {
  // themeContext 有完整的类型提示
  console.log(themeContext.theme.value)
}

2. 泛型组件

<!-- components/List.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{
  items: T[]
  renderItem: (item: T) => any
  keyExtractor?: (item: T) => string
}>()
</script>

<template>
  <div class="list">
    <div v-for="item in items" :key="keyExtractor?.(item) ?? item.id">
      <slot name="item" :item="item">
        {{ renderItem?.(item) }}
      </slot>
    </div>
  </div>
</template>

<!-- 使用示例 -->
<script setup lang="ts">
interface User {
  id: number
  name: string
}

const users = ref<User[]>([/* ... */])
</script>

<template>
  <List :items="users" :keyExtractor="(user) => `user-${user.id}`">
    <template #item="{ item }">
      {{ item.name }}
    </template>
  </List>
</template>

遵循这些实践,可以让你的 Vue 3 + TypeScript 项目更加健壮、可维护,同时提供优秀的开发体验。建议从项目一开始就建立这些规范,并在团队中推广使用。