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/