引言
Vue 3 的发布带来了 Composition API、Performance 提升等重磅更新,结合 TypeScript 的类型系统,可以构建出更加健壮、可维护的企业级应用。本文将通过实战项目,带你掌握 Vue 3 + TypeScript 的核心技术。
一、项目初始化
1.1 使用 Vite 创建项目
# 创建 Vue 3 + TypeScript 项目
npm create vite@latest my-app -- --template vue-ts
# 安装依赖
cd my-app
npm install
# 安装常用工具库
npm install vue-router pinia axios
npm install -D @types/node
1.2 项目结构规范
src/
├── api/ # API 接口
│ ├── modules/ # 按模块划分
│ └── index.ts # 统一导出
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── base/ # 基础组件
│ └── business/ # 业务组件
├── composables/ # 组合式函数
├── hooks/ # 自定义 hooks
├── layouts/ # 布局组件
├── router/ # 路由配置
├── stores/ # Pinia 状态管理
├── styles/ # 全局样式
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
├── views/ # 页面组件
├── App.vue
└── main.ts
二、TypeScript 类型定义
2.1 基础类型定义
// types/user.ts
export interface User {
id: number;
username: string;
email: string;
role: "admin" | "user" | "guest";
createdAt: Date;
profile?: UserProfile;
}
export interface UserProfile {
avatar?: string;
bio?: string;
phone?: string;
}
// types/api.ts
export interface ApiResponse {
code: number;
message: string;
data: T;
}
export interface PageParams {
page: number;
pageSize: number;
}
export interface PageResult {
list: T[];
total: number;
page: number;
pageSize: number;
}
2.2 组件 Props 类型
<script setup lang="ts">
import type { User } from "@/types/user";
interface Props {
userInfo: User;
showAvatar?: boolean;
maxLength?: number;
}
const props = withDefaults(defineProps<Props>(), {
showAvatar: true,
maxLength: 100
});
interface Emits {
(e: "update", value: string): void;
(e: "delete", id: number): void;
}
const emit = defineEmits<Emits>();
</script>
三、Composition API 实战
3.1 基础组合式函数
// composables/useLoading.ts
import { ref, type Ref } from "vue";
export function useLoading(initialValue = false) {
const loading = ref(initialValue);
const startLoading = () => {
loading.value = true;
};
const stopLoading = () => {
loading.value = false;
};
const withLoading = async <T>(fn: () => Promise<T>): Promise<T> => {
startLoading();
try {
return await fn();
} finally {
stopLoading();
}
};
return {
loading,
startLoading,
stopLoading,
withLoading
};
}
3.2 数据请求组合式函数
// composables/useFetch.ts
import { ref, type Ref } from "vue";
import type { ApiResponse } from "@/types/api";
interface UseFetchOptions {
immediate?: boolean;
onError?: (error: Error) => void;
}
export function useFetch<T>(
url: string,
options: UseFetchOptions = {}
) {
const { immediate = true, onError } = options;
const data: Ref<T | null> = ref(null);
const error: Ref<Error | null> = ref(null);
const loading = ref(false);
const execute = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
const result: ApiResponse<T> = await response.json();
if (result.code === 200) {
data.value = result.data;
} else {
throw new Error(result.message);
}
} catch (e) {
error.value = e as Error;
onError?.(error.value);
} finally {
loading.value = false;
}
};
if (immediate) {
execute();
}
return {
data,
error,
loading,
execute,
refresh: execute
};
}
3.3 表单处理组合式函数
// composables/useForm.ts
import { ref, reactive, type Ref } from "vue";
interface ValidationRule {
required?: boolean;
pattern?: RegExp;
minLength?: number;
maxLength?: number;
validator?: (value: any) => boolean | string;
}
interface FieldConfig {
rules?: ValidationRule[];
}
export function useForm<T extends Record<string, any>>(
initialValues: T,
validationConfig: Record<keyof T, FieldConfig> = {}
) {
const form = reactive({ ...initialValues });
const errors: Ref<Record<keyof T, string>> = ref({} as any);
const isSubmitting = ref(false);
const validateField = (field: keyof T): boolean => {
const rules = validationConfig[field]?.rules || [];
const value = form[field];
for (const rule of rules) {
if (rule.required && !value) {
errors.value[field] = "此字段为必填项";
return false;
}
if (rule.pattern && !rule.pattern.test(value)) {
errors.value[field] = "格式不正确";
return false;
}
if (rule.minLength && value.length < rule.minLength) {
errors.value[field] = `最少需要${rule.minLength}个字符`;
return false;
}
if (rule.validator) {
const result = rule.validator(value);
if (result !== true) {
errors.value[field] = typeof result === "string" ? result : "验证失败";
return false;
}
}
}
errors.value[field] = "";
return true;
};
const validate = (): boolean => {
let isValid = true;
Object.keys(form).forEach((key) => {
if (!validateField(key as keyof T)) {
isValid = false;
}
});
return isValid;
};
const reset = () => {
Object.assign(form, initialValues);
errors.value = {} as any;
};
return {
form,
errors,
isSubmitting,
validate,
validateField,
reset
};
}
四、Pinia 状态管理
4.1 Store 定义
// stores/user.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import type { User } from "@/types/user";
export const useUserStore = defineStore("user", () => {
// State
const user = ref<User | null>(null);
const token = ref<string>("");
const isLoggedIn = computed(() => !!token.value);
// Actions
const login = async (username: string, password: string) => {
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (result.code === 200) {
token.value = result.data.token;
user.value = result.data.user;
localStorage.setItem("token", result.data.token);
}
};
const logout = () => {
user.value = null;
token.value = "";
localStorage.removeItem("token");
};
const fetchUserInfo = async () => {
const response = await fetch("/api/user/info", {
headers: { Authorization: `Bearer ${token.value}` }
});
const result = await response.json();
user.value = result.data;
};
return {
user,
token,
isLoggedIn,
login,
logout,
fetchUserInfo
};
});
4.2 在组件中使用
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
// 访问 state
console.log(userStore.user);
console.log(userStore.isLoggedIn);
// 调用 action
await userStore.login("username", "password");
await userStore.fetchUserInfo();
</script>
五、路由配置
5.1 路由类型定义
// types/router.ts
import type { RouteLocationNormalized } from "vue-router";
export type RouteMeta = {
title?: string;
requiresAuth?: boolean;
roles?: Array<"admin" | "user">;
keepAlive?: boolean;
};
export type AppRouteRecord = RouteRecordRaw & {
meta?: RouteMeta;
children?: AppRouteRecord[];
};
5.2 路由守卫
// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import type { RouteMeta } from "@/types/router";
import { useUserStore } from "@/stores/user";
const routes: AppRouteRecord[] = [
{
path: "/",
component: () => import("@/layouts/DefaultLayout.vue"),
children: [
{
path: "",
name: "Home",
component: () => import("@/views/Home.vue"),
meta: { title: "首页" }
},
{
path: "dashboard",
name: "Dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { title: "仪表盘", requiresAuth: true }
}
]
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
meta: { title: "登录" }
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const meta = to.meta as RouteMeta;
// 设置页面标题
document.title = meta.title ? `${meta.title} - 应用名称` : "应用名称";
// 检查是否需要登录
if (meta.requiresAuth && !userStore.isLoggedIn) {
next({ name: "Login", query: { redirect: to.fullPath } });
return;
}
// 检查角色权限
if (meta.roles && !meta.roles.includes(userStore.user?.role)) {
next({ name: "403" });
return;
}
next();
});
export default router;
六、API 请求封装
6.1 Axios 实例配置
// api/index.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios";
import type { ApiResponse } from "@/types/api";
import { useUserStore } from "@/stores/user";
const baseURL = import.meta.env.VITE_API_BASE_URL || "/api";
const apiClient: AxiosInstance = axios.create({
baseURL,
timeout: 10000,
headers: {
"Content-Type": "application/json"
}
});
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
const userStore = useUserStore();
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
apiClient.interceptors.response.use(
(response) => {
const { data } = response;
if (data.code !== 200) {
return Promise.reject(new Error(data.message));
}
return data;
},
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore();
userStore.logout();
window.location.href = "/login";
}
return Promise.reject(error);
}
);
// 封装请求方法
export const request = <T = any>(
config: AxiosRequestConfig
): Promise<ApiResponse<T>> => {
return apiClient(config);
};
export const get = <T = any>(url: string, config?: AxiosRequestConfig) =>
request<T>({ method: "GET", url, ...config });
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
request<T>({ method: "POST", url, data, ...config });
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
request<T>({ method: "PUT", url, data, ...config });
export const del = <T = any>(url: string, config?: AxiosRequestConfig) =>
request<T>({ method: "DELETE", url, ...config });
6.2 API 模块示例
// api/modules/user.ts
import { get, post, put, del } from "@/api";
import type { User, PageParams, PageResult } from "@/types";
export const userApi = {
// 获取用户列表
getList: (params: PageParams) =>
get<PageResult<User>>("/users", { params }),
// 获取用户详情
getDetail: (id: number) =>
get<User>(`/users/${id}`),
// 创建用户
create: (data: Omit<User, "id" | "createdAt">) =>
post<User>("/users", data),
// 更新用户
update: (id: number, data: Partial<User>) =>
put<User>(`/users/${id}`, data),
// 删除用户
delete: (id: number) =>
del(`/users/${id}`)
};
七、组件开发最佳实践
7.1 基础组件示例
<!-- components/base/AppButton.vue -->
<template>
<button
:class="[
"app-button",
`app-button--${type}`,
`app-button--${size}`,
{ "app-button--loading": loading }
]"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="app-button__spinner"></span>
<slot></slot>
</button>
</template>
<script setup lang="ts">
interface Props {
type?: "primary" | "success" | "warning" | "danger" | "default";
size?: "small" | "medium" | "large";
disabled?: boolean;
loading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
type: "default",
size: "medium",
disabled: false,
loading: false
});
const emit = defineEmits<{
click: [event: MouseEvent];
}>();
const handleClick = (event: MouseEvent) => {
if (!props.disabled && !props.loading) {
emit("click", event);
}
};
</script>
<style scoped>
.app-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.app-button--primary {
background-color: #409eff;
color: white;
}
.app-button--medium {
padding: 8px 16px;
font-size: 14px;
}
.app-button--loading {
opacity: 0.7;
cursor: not-allowed;
}
</style>
八、性能优化
8.1 组件懒加载
// 路由懒加载
const Dashboard = () => import("@/views/Dashboard.vue");
// 组件异步加载
const HeavyComponent = defineAsyncComponent(() =>
import("@/components/business/HeavyComponent.vue")
);
8.2 列表虚拟滚动
<script setup lang="ts">
import { computed, ref } from "vue";
interface Props {
items: any[];
itemHeight?: number;
}
const props = withDefaults(defineProps<Props>(), {
itemHeight: 50
});
const containerRef = ref<HTMLElement | null>(null);
const scrollTop = ref(0);
const visibleCount = ref(10);
const visibleItems = computed(() => {
const start = Math.floor(scrollTop.value / props.itemHeight);
const end = start + visibleCount.value;
return props.items.slice(start, end);
});
const totalHeight = computed(() => {
return props.items.length * props.itemHeight;
});
</script>
总结
Vue 3 + TypeScript 的组合为企业级应用开发提供了强大的工具链。通过本文的实战指南,你掌握了:
- 项目架构:规范化的目录结构和类型定义
- Composition API:可复用的组合式函数
- 状态管理:Pinia Store 的最佳实践
- 路由系统:类型安全的路由配置和守卫
- API 封装:统一的请求拦截和错误处理
- 组件开发:可复用的基础组件
记住:类型系统是你的朋友,不是敌人。投入时间完善类型定义,会在后期维护中获得巨大回报。持续学习 Vue 3 的新特性,保持代码的简洁和可维护性,是成为前端高手的关键。
文章评论