Vue3 + TypeScript:完整集成指南
恭喜你走到最后一期!前面我们学习了 ES6、Vite、Vue3 基础、组合式 API 和 TypeScript。现在,让我们把 TypeScript 完整集成到 Vue3 项目中。
项目创建
Vite + Vue + TypeScript
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
项目结构
src/
├── main.ts
├── App.vue
├── vite-env.d.ts
├── components/
│ └── HelloWorld.vue
├── views/
├── router/
│ └── index.ts
├── stores/
│ └── user.ts
├── types/
│ └── index.ts
├── api/
│ └── user.ts
├── composables/
│ └── useUser.ts
└── utils/
└── format.ts
类型定义
全局类型
// types/index.ts
export interface User {
id: number
name: string
email: string
avatar?: string
}
export interface ApiResponse {
code: number
message: string
data: T
}
export type Theme = 'light' | 'dark'
export interface AppState {
user: User | null
theme: Theme
loading: boolean
}
组件 Props 类型
interface Props {
title: string
count?: number
users: User[]
}
const props = withDefaults(defineProps(), {
count: 0
})
组件 Emits 类型
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: number): void
(e: 'change', event: Event): void
}
const emit = defineEmits()
function handleUpdate() {
emit('update', 'new value')
}
组合式 API + TypeScript
ref
import { ref } from 'vue'
// 类型推断
const count = ref(0)
const name = ref('Alice')
// 显式类型
const user = ref(null)
const items = ref([])
reactive
import { reactive } from 'vue'
interface State {
count: number
name: string
items: string[]
}
const state = reactive({
count: 0,
name: '',
items: []
})
computed
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 自动推断返回类型
const fullName = computed(() => <code>${firstName.value} ${lastName.value})
// 显式类型
const userCount = computed(() => users.value.length)
组合式函数
// composables/useUser.ts
import { ref, computed } from 'vue'
import type { User } from '@/types'
export function useUser() {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const isLoggedIn = computed(() => !!user.value)
const userName = computed(() => user.value?.name ?? 'Guest')
async function fetchUser(id: number) {
loading.value = true
error.value = null
try {
const response = await fetch(<code>/api/users/${id})
user.value = await response.json()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
}
function logout() {
user.value = null
}
return {
user,
loading,
error,
isLoggedIn,
userName,
fetchUser,
logout
}
}
Pinia + TypeScript
Store 类型定义
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'
export const useUserStore = defineStore('user', () => {
// State
const user = ref(null)
const token = ref(localStorage.getItem('token'))
// Getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name ?? 'Guest')
// Actions
async function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await response.json()
user.value = data.user
token.value = data.token
localStorage.setItem('token', data.token)
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem('token')
}
return {
user,
token,
isLoggedIn,
userName,
login,
logout
}
}, {
persist: true
})
使用 Store
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)
// Actions
const { login, logout } = userStore
// 表单
const email = ref('')
const password = ref('')
async function handleLogin() {
await login(email.value, password.value)
}
<div>
<p>Welcome, {{ userName }}</p>
<button>Logout</button>
</div>
<button type="submit">Login</button>
Vue Router + TypeScript
路由类型
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/users/:id',
name: 'User',
component: () => import('@/views/User.vue'),
props: true,
meta: {
requiresAuth: true
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
路由元信息类型
// types/router.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
roles?: string[]
}
}
使用路由
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 获取参数(类型安全)
const userId = computed(() => Number(route.params.id))
// 编程式导航
function goToUser(id: number) {
router.push({ name: 'User', params: { id } })
}
API 集成
类型安全的 API 客户端
// api/client.ts import type { ApiResponse } from '@/types' class ApiClient { private baseUrl: string constructor(baseUrl: string) { this.baseUrl = baseUrl } private async request( path: string, options?: RequestInit ): Promise { const response = await fetch(<code>${this.baseUrl}${path}, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers } }) if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } return response.json() } get(path: string): Promise { return this.request(path) } post(path: string, data: unknown): Promise { return this.request(path, { method: 'POST', body: JSON.stringify(data) }) } put(path: string, data: unknown): Promise { return this.request(path, { method: 'PUT', body: JSON.stringify(data) }) } delete(path: string): Promise { return this.request(path, { method: 'DELETE' }) } } export const api = new ApiClient('/api')
API 模块
// api/user.ts import { api } from './client' import type { User, ApiResponse } from '@/types' export const userApi = { getAll(): Promise { return api.get<ApiResponse>('/users').then(r => r.data) }, getById(id: number): Promise { return api.get<ApiResponse>(<code>/users/${id}).then(r => r.data) }, create(data: Omit): Promise { return api.post<ApiResponse>('/users', data).then(r => r.data) }, update(id: number, data: Partial): Promise { return api.put<ApiResponse>(/users/${id}, data).then(r => r.data) }, delete(id: number): Promise { return api.delete(/users/${id}) } }
使用 API
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import type { User } from '@/types'
const users = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
users.value = await userApi.getAll()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
}
onMounted(fetchUsers)
<div>Loading...</div>
<div>{{ error }}</div>
<ul>
<li>
{{ user.name }}
</li>
</ul>
表单处理
类型安全的表单
import { ref, reactive } from 'vue'
interface FormData {
name: string
email: string
age: number
subscribe: boolean
}
interface FormErrors {
name?: string
email?: string
age?: string
}
const form = reactive({
name: '',
email: '',
age: 0,
subscribe: false
})
const errors = reactive({})
function validate(): boolean {
let isValid = true
if (!form.name.trim()) {
errors.name = 'Name is required'
isValid = false
}
if (!form.email.includes('@')) {
errors.email = 'Invalid email'
isValid = false
}
if (form.age 150) {
errors.age = 'Invalid age'
isValid = false
}
return isValid
}
async function handleSubmit() {
if (!validate()) return
// 提交表单
await submitForm(form)
}
<div>
<span>{{ errors.name }}</span>
</div>
<div>
<span>{{ errors.email }}</span>
</div>
<div>
<span>{{ errors.age }}</span>
</div>
<div>
<label>
Subscribe to newsletter
</label>
</div>
<button type="submit">Submit</button>
使用 vee-validate
npm install vee-validate zod @vee-validate/zod
import { useForm, useField } from 'vee-validate'
import { z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
const schema = toTypedSchema(
z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
age: z.number().min(0).max(150)
})
)
const { handleSubmit, errors } = useForm({
validationSchema: schema
})
const { value: name } = useField('name')
const { value: email } = useField('email')
const { value: age } = useField('age')
const onSubmit = handleSubmit((values) => {
console.log(values)
})
工具类型
组件实例类型
import type { ComponentInstance } from 'vue'
// 获取组件实例类型
type MyComponentInstance = ComponentInstance
提取 Props 类型
import type { ExtractPropTypes } from 'vue'
const propsDefinition = {
title: String,
count: {
type: Number,
default: 0
}
} as const
type Props = ExtractPropTypes
// { title: string; count: number }
组件自定义类型
// 自定义组件类型
declare module '@vue/runtime-core' {
export interface GlobalComponents {
MyButton: typeof import('@/components/MyButton.vue')['default']
MyInput: typeof import('@/components/MyInput.vue')['default']
}
}
测试
Vitest 配置
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true
}
})
组件测试
// components/__tests__/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../Button.vue'
describe('Button', () => {
it('renders with text', () => {
const wrapper = mount(Button, {
props: { text: 'Click me' }
})
expect(wrapper.text()).toBe('Click me')
})
it('emits click event', async () => {
const wrapper = mount(Button, {
props: { text: 'Click me' }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
})
最佳实践
1. 使用 script setup
// 推荐
2. 类型优先
// 先定义类型,再实现
interface User {
id: number
name: string
}
const user = ref(null)
3. 使用组合式函数
// 提取可复用逻辑
export function useUser() {
// ...
}
4. 严格模式
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
5. 类型安全的 Props
interface Props {
title: string
count?: number
}
const props = withDefaults(defineProps(), {
count: 0
})
系列总结
恭喜你完成了从 ES6 到 Vue3 + TypeScript 的完整学习之旅!
第一期:TypeScript 基础类型系统
第二期:接口、泛型与类型体操
第三期:项目配置与最佳实践
第四期:TypeScript 与前端框架
第五期:Vue3 + TypeScript 完整集成
你现在掌握了:
- TypeScript 类型系统
- Vue3 组合式 API
- 类型安全的组件开发
- Pinia 状态管理
- Vue Router 类型安全
- API 集成最佳实践
下一步:
- 学习 Nuxt 3(Vue3 全栈框架)
- 探索 Vite 插件开发
- 学习服务端渲染
- 阅读官方文档
进阶资源:
恭喜!你已经掌握了 Vue3 + TypeScript 的核心技能。去构建你的下一个项目吧!
Views: 0
