Vue3 Composition API与TypeScript实战:组件逻辑复用与类型安全方案

Vue3的Composition API搭配TypeScript已成为前端工程化的标准实践。相比Options API,Composition API通过setup函数将相关逻辑聚合在一起,解决了大型组件中逻辑分散的问题。本文以实际项目案例展示组件逻辑复用、类型推导和性能优化的完整方案。

项目搭建与TypeScript配置

使用Vite创建Vue3+TypeScript项目:

npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install pinia vue-router axios

tsconfig.json的关键配置:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["vite/client", "node"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}

strict: true启用了所有严格类型检查。对于Vue3项目推荐保持开启,配合vue-tsc在构建阶段捕获类型错误。

Composition API核心模式:逻辑复用

逻辑复用是Composition API最大的优势。将可复用逻辑抽取为composable函数,任何组件都可以引入使用。一个通用分页Hook的实现:

// src/composables/usePagination.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'

interface PaginationOptions {
  pageSize?: number
  initialPage?: number
}

export function usePagination<T>(
  sourceData: Ref<T[]>,
  options: PaginationOptions = {}
) {
  const currentPage = ref(options.initialPage ?? 1)
  const pageSize = ref(options.pageSize ?? 10)
  const total = ref(0)

  const totalPages = computed(() => 
    Math.ceil(total.value / pageSize.value)
  )

  const paginatedData = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value
    const end = start + pageSize.value
    return sourceData.value.slice(start, end)
  })

  const goToPage = (page: number) => {
    const clamped = Math.max(1, Math.min(page, totalPages.value))
    currentPage.value = clamped
  }

  const nextPage = () => goToPage(currentPage.value + 1)
  const prevPage = () => goToPage(currentPage.value - 1)
  const setTotal = (val: number) => { total.value = val }

  return {
    currentPage, pageSize, total, totalPages,
    paginatedData, goToPage, nextPage, prevPage, setTotal
  }
}

composable函数的命名规范是以use开头,返回reactive引用和操作方法。泛型参数T使分页Hook适用于任意数据类型。在组件中使用:

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { usePagination } from '@/composables/usePagination'
import { fetchUsers, type User } from '@/api/user'

const users = ref<User[]>([])
const loading = ref(false)

const {
  currentPage, pageSize, total, totalPages,
  paginatedData, goToPage, nextPage, prevPage, setTotal
} = usePagination(users, { pageSize: 20 })

const loadUsers = async () => {
  loading.value = true
  const res = await fetchUsers({
    page: currentPage.value,
    size: pageSize.value
  })
  users.value = res.list
  setTotal(res.total)
  loading.value = false
}

onMounted(loadUsers)
</script>

TypeScript类型安全的API请求层

// src/api/request.ts
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'

interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 10000
})

request.interceptors.request.use((config) => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

request.interceptors.response.use(
  (response: AxiosResponse<ApiResponse<unknown>>) => {
    if (response.data.code !== 0) {
      return Promise.reject(new Error(response.data.message))
    }
    return response.data.data
  },
  (error) => {
    if (error.response?.status === 401) {
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
  return request.get(url, config)
}

export async function post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
  return request.post(url, data, config)
}

API实体类型定义使用Omit和Partial等工具类型从基础类型自动派生:

// src/api/user.ts
export interface User {
  id: number
  username: string
  email: string
  role: 'admin' | 'user' | 'guest'
  createdAt: string
  avatar?: string
}

export interface GetUserListParams {
  page: number
  size: number
  keyword?: string
  role?: User['role']
}

export interface PaginatedResult<T> {
  list: T[]
  total: number
}

export const userApi = {
  getList: (params: GetUserListParams) => 
    get<PaginatedResult<User>>('/users', { params }),
  
  create: (data: Omit<User, 'id' | 'createdAt'>) => 
    post<User>('/users', data),
  
  update: (id: number, data: Partial<Pick<User, 'username' | 'email' | 'role'>>) => 
    post<User>(`/users/${id}`, data)
}

使用Omit和Partial从User类型自动派生出创建和更新的参数类型,避免重复定义。当User结构变更时,所有相关类型自动同步。

组件Props与Emits的类型定义

<script setup lang="ts">
interface Props {
  user: User | null
  mode: 'create' | 'edit'
  loading?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  user: null
})

interface Emits {
  (e: 'submit', data: Partial<User>): void
  (e: 'cancel'): void
}

const emit = defineEmits<Emits>()

const isVisible = defineModel<boolean>('visible', { default: false })
</script>

Vue3.4+的defineModel宏简化了v-model的实现,无需手动声明update事件。withDefaults为可选props提供默认值,类型推导自动将可选属性标记为可选。

Web性能优化:虚拟列表实现

大数据量列表渲染是Web性能优化的典型场景。虚拟列表只渲染可视区域的元素,DOM节点数恒定:

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

interface Props {
  items: T[]
  itemHeight: number
  height: number
  buffer?: number
}

const props = withDefaults(defineProps<Props>(), { buffer: 5 })
const containerRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)

const visibleRange = computed(() => {
  const start = Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.buffer)
  const visibleCount = Math.ceil(props.height / props.itemHeight) + props.buffer * 2
  const end = Math.min(props.items.length, start + visibleCount)
  return { start, end }
})

const visibleItems = computed(() => {
  const { start, end } = visibleRange.value
  return props.items.slice(start, end).map((item, i) => ({
    item, index: start + i
  }))
})

const totalHeight = computed(() => props.items.length * props.itemHeight)
const offsetY = computed(() => visibleRange.value.start * props.itemHeight)

const handleScroll = (e: Event) => {
  scrollTop.value = (e.target as HTMLElement).scrollTop
}
</script>

<template>
  <div ref="containerRef" :style="{ height: height + 'px', overflow: 'auto' }">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div v-for="{ item, index } in visibleItems" :key="index"
             :style="{ height: itemHeight + 'px' }">
          <slot :item="item" :index="index" />
        </div>
      </div>
    </div>
  </div>
</template>

generic=”T”语法是Vue3.3+引入的泛型组件支持,使VirtualList组件保持完整的类型安全。使用时插槽内容自动获得正确的item类型推导。虚拟列表的关键参数是buffer(缓冲区行数),通常设为3到5即可平衡性能和体验。

响应式布局与CSS变量联动

响应式布局在Vue3中可以通过CSS变量与Composition API联动实现动态主题:

// src/composables/useTheme.ts
import { ref, watchEffect } from 'vue'

type ThemeMode = 'light' | 'dark' | 'auto'
const themeMode = ref<ThemeMode>(localStorage.getItem('theme') as ThemeMode || 'auto')

export function useTheme() {
  const applyTheme = (mode: ThemeMode) => {
    const root = document.documentElement
    const isDark = mode === 'dark' || 
      (mode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
    root.classList.toggle('dark', isDark)
    localStorage.setItem('theme', mode)
  }

  watchEffect(() => applyTheme(themeMode.value))
  return { themeMode }
}

CSS变量定义在:root和.dark选择器中,Tailwind等CSS框架可以无缝集成。这种方式的好处是主题切换不触发Vue的响应式更新和组件重渲染,性能开销最小。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3compositionapi-yu-typescript-shi-zhan-zu-jian-luo-ji-fu/

(0)
小编小编
上一篇 17小时前
下一篇 17小时前

相关推荐