前端开发学习(VUE+TS)有这一篇就够!
1. 引言:为什么选择 Vue 3 + TypeScript?
在当今的前端开发领域,Vue.js 以其渐进式、易上手和功能强大的特性,成为了最受欢迎的框架之一。而 TypeScript 作为 JavaScript 的超集,提供了强大的静态类型系统,极大地提升了代码的可维护性、可读性和开发体验。Vue 3 与 TypeScript 的结合,堪称现代前端开发的“黄金搭档”。
学习路径建议:
- 掌握基础:HTML、CSS、JavaScript (ES6+)。
- 框架入门:Vue 3 核心概念(响应式、组合式 API、组件)。
- 类型加持:TypeScript 基础类型、接口、泛型。
- 工程实践:构建工具、状态管理、路由、测试。
- 生态深入:UI 库、工具链、性能优化。
本文将带你从零开始,系统性地掌握 Vue 3 + TypeScript 的全栈开发技能,并提供丰富的官方资源链接和可运行的代码示例。
2. 环境搭建与项目创建
2.1 安装 Node.js 与包管理器
Vue 的开发离不开 Node.js 环境。请前往 Node.js 官网 下载并安装最新的 LTS(长期支持)版本。安装完成后,在终端中验证:
node --version
npm --version
推荐使用更快的包管理器 pnpm 或 yarn:
# 安装 pnpm
npm install -g pnpm
pnpm --version
2.2 使用 Vite 创建 Vue + TypeScript 项目
Vite 是新一代的前端构建工具,启动速度和热更新极快,是 Vue 官方推荐的构建工具。
通过以下命令创建项目:
# 使用 npm
npm create vue@latest
# 或使用 pnpm
pnpm create vue@latest
在交互式命令行中,根据提示进行选择:
- Project name:
vue-ts-demo - Add TypeScript?:
Yes - Add JSX Support?: 按需选择
- Add Vue Router for Single Page Application development?:
Yes(推荐) - Add Pinia for state management?:
Yes(推荐) - Add Vitest for Unit Testing?: 按需选择
- Add an End-to-End Testing Solution?: 按需选择
- Add ESLint for code quality?:
Yes(推荐)
项目创建完成后,进入目录并安装依赖:
cd vue-ts-demo
pnpm install # 或 npm install
启动开发服务器:
pnpm dev
打开浏览器访问 http://localhost:5173,你将看到 Vue 的欢迎页面。
2.3 项目结构概览
vue-ts-demo/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 图片、字体等资源
│ ├── components/ # 可复用组件
│ ├── views/ # 页面级组件 (配合路由)
│ ├── router/ # 路由配置 (index.ts)
│ ├── stores/ # Pinia 状态管理仓库
│ ├── App.vue # 根组件
│ └── main.ts # 应用入口文件
├── index.html # HTML 模板
├── package.json # 项目依赖和脚本
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
└── README.md
3. TypeScript 基础快速入门
在深入 Vue 之前,需要掌握一些 TypeScript 的核心概念,这些将在 Vue 组件中频繁使用。
3.1 基本类型与类型注解
TypeScript 扩展了 JavaScript 的类型系统。
// 变量类型注解
let username: string = '张三';
let age: number = 25;
let isStudent: boolean = true;
let hobbies: string[] = ['篮球', '音乐', '编程'];
let scores: Array<number> = [90, 85, 95]; // 泛型数组
// 元组 (Tuple) - 固定长度和类型的数组
let person: [string, number] = ['李四', 30];
// 枚举 (Enum)
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
let move: Direction = Direction.Up;
// 任意类型 (Any) - 应谨慎使用
let dynamicData: any = '可以是任何东西';
dynamicData = 42;
3.2 接口 (Interface) 与类型别名 (Type Alias)
用于定义对象的形状,是 TypeScript 的精华所在。
// 接口定义对象结构
interface User {
id: number;
name: string;
email: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
}
// 使用接口
const currentUser: User = {
id: 1,
name: '王五',
email: 'wangwu@example.com',
createdAt: new Date()
};
// currentUser.createdAt = new Date(); // 错误!只读属性不能修改
// 类型别名
type ID = number | string; // 联合类型
type Callback = (data: string) => void; // 函数类型
// 在 Vue 组件中,我们常用接口来定义 Props 和 Emits 的类型
3.3 泛型 (Generics)
泛型让组件支持多种类型,提高复用性。
// 一个简单的泛型函数
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("hello"); // 显式指定类型
let output2 = identity(42); // 类型推断为 number
// 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T; // 响应数据的类型由外部决定
}
const userResponse: ApiResponse<User> = {
code: 200,
message: '成功',
data: currentUser
};
4. Vue 3 核心概念与组合式 API
Vue 3 引入了组合式 API (Composition API),它比 Vue 2 的选项式 API 更灵活,尤其适合复杂组件和 TypeScript 集成。
4.1 响应式基础:ref 与 reactive
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
// ref: 用于定义响应式的基本类型值 (返回一个带有 .value 属性的对象)
const count = ref<number>(0); // 显式指定泛型类型为 number
const message = ref('Hello Vue 3'); // 类型推断为 string
// reactive: 用于定义响应式的对象
interface UserState {
name: string;
age: number;
}
const state = reactive<UserState>({
name: '小明',
age: 20
});
// 在模板中,ref 会自动解包,无需 .value
// 在脚本中,需要 .value 访问
const increment = () => {
count.value++; // 正确
// count++ // 错误!不能直接操作
state.age += 1; // reactive 对象属性可直接修改
};
</script>
<template>
<div>
<p>{{ count }}</p> <!-- 自动解包,显示 0 -->
<p>{{ state.name }} 今年 {{ state.age }} 岁</p>
<button @click="increment">增加</button>
</div>
</template>
4.2 计算属性与侦听器
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
const price = ref<number>(100);
const quantity = ref<number>(2);
// 计算属性:基于响应式依赖进行缓存计算
const totalPrice = computed<number>(() => {
return price.value * quantity.value;
});
// 侦听器:监听响应式数据的变化
watch(quantity, (newVal, oldVal) => {
console.log(`数量从 ${oldVal} 变为 ${newVal}`);
// 可以执行副作用,如发送请求、操作 DOM 等
});
// 深度侦听对象
const userInfo = ref({ name: '张三', details: { age: 25 } });
watch(
userInfo,
(newVal) => {
console.log('用户信息变化:', newVal);
},
{ deep: true } // 深度监听
);
</script>
4.3 生命周期钩子
组合式 API 中使用 onXxx 函数注册生命周期钩子。
<script setup lang="ts">
import { onMounted, onUpdated, onUnmounted } from 'vue';
onMounted(() => {
console.log('组件挂载完成,可以访问 DOM');
// 常用于初始化数据、订阅事件、调用接口
});
onUpdated(() => {
console.log('组件更新完成');
});
onUnmounted(() => {
console.log('组件即将卸载');
// 清理定时器、取消订阅、释放资源
});
</script>
5. 组件开发:Props、Emits 与 Slots
5.1 使用 TypeScript 定义 Props
使用 defineProps 宏函数,并配合接口或类型字面量来获得完整的类型推断和校验。
<!-- ChildComponent.vue -->
<script setup lang="ts">
// 方式1:使用接口
interface Props {
title: string;
count?: number; // 可选属性
isActive: boolean;
list: string[];
onAction?: () => void; // 函数类型
}
const props = defineProps<Props>();
// 方式2:使用运行时声明与类型注解(Vue 3.3+ 推荐)
// 可以定义默认值,但语法稍复杂
// import { withDefaults } from 'vue';
// interface Props { title: string; count?: number; }
// const props = withDefaults(defineProps<Props>(), {
// count: 0
// });
// 使用 props
console.log(props.title);
</script>
<template>
<div :class="{ active: isActive }">
<h3>{{ title }}</h3>
<p>数量: {{ count }}</p>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</template>
5.2 使用 TypeScript 定义 Emits
使用 defineEmits 宏函数来定义组件可以触发的事件及其载荷类型。
<!-- ChildComponent.vue -->
<script setup lang="ts">
// 定义事件及其载荷类型
interface Emits {
(e: 'update:title', value: string): void;
(e: 'submit', payload: { id: number; data: string }): void;
(e: 'cancel'): void; // 无载荷事件
}
const emit = defineEmits<Emits>();
const handleClick = () => {
emit('update:title', '新的标题');
emit('submit', { id: 1, data: '提交的数据' });
};
</script>
<template>
<button @click="handleClick">触发事件</button>
</template>
在父组件中使用:
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const title = ref('初始标题');
const handleSubmit = (payload: { id: number; data: string }) => {
console.log('收到提交:', payload);
};
</script>
<template>
<ChildComponent
:title="title"
@update:title="title = $event"
@submit="handleSubmit"
/>
</template>
5.3 插槽 (Slots) 与作用域插槽
<!-- LayoutComponent.vue -->
<script setup lang="ts">
import { useSlots } from 'vue';
// 检查插槽内容
const slots = useSlots();
const hasHeader = !!slots.header;
</script>
<template>
<div class="layout">
<header v-if="hasHeader">
<slot name="header"></slot>
</header>
<main>
<!-- 默认插槽 -->
<slot></slot>
</main>
<footer>
<!-- 作用域插槽:向父组件传递数据 -->
<slot name="footer" :year="2024" :author="'Vue Team'"></slot>
</footer>
</div>
</template>
使用插槽的父组件:
<template>
<LayoutComponent>
<template #header>
<h1>这是页头</h1>
</template>
<p>这是默认插槽的内容。</p>
<template #footer="{ year, author }">
<p>© {{ year }} {{ author }}. All rights reserved.</p>
</template>
</LayoutComponent>
</template>
6. 路由管理:Vue Router 4
6.1 安装与配置
如果创建项目时未选择 Vue Router,可以手动安装:
pnpm add vue-router@4
创建路由配置文件 src/router/index.ts:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import AboutView from '../views/AboutView.vue';
import UserProfile from '../views/UserProfile.vue';
// 定义路由记录的类型
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: HomeView,
meta: {
requiresAuth: false, // 路由元信息,可用于权限控制
title: '首页'
}
},
{
path: '/about',
name: 'About',
component: AboutView,
meta: { title: '关于我们' }
},
{
path: '/user/:id', // 动态路由
name: 'UserProfile',
component: UserProfile,
props: true // 将路由参数作为 props 传递给组件
},
{
path: '/:pathMatch(.*)*', // 404 页面捕获
name: 'NotFound',
component: () => import('../views/NotFound.vue') // 路由懒加载
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // 使用 HTML5 History 模式
routes
});
// 全局前置守卫
router.beforeEach((to, from) => {
// 修改页面标题
const pageTitle = to.meta.title as string;
if (pageTitle) {
document.title = `${pageTitle} - 我的 Vue 应用`;
}
// 可以在此进行登录验证等
// if (to.meta.requiresAuth && !isAuthenticated) {
// return { name: 'Login' };
// }
});
export default router;
在 main.ts 中注册路由:
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
6.2 在组件中使用路由
<!-- App.vue -->
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router';
</script>
<template>
<header>
<nav>
<!-- 声明式导航 -->
<RouterLink to="/">首页</RouterLink>
<RouterLink :to="{ name: 'About' }">关于</RouterLink>
<RouterLink :to="`/user/${userId}`">我的主页</RouterLink>
</nav>
</header>
<main>
<!-- 路由出口 -->
<RouterView />
</main>
</template>
<!-- UserProfile.vue -->
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
// 通过 useRoute 获取当前路由信息(只读)
const route = useRoute();
// 路由参数是字符串类型,需要转换
const userId = computed(() => Number(route.params.id) || 0);
// 通过 useRouter 进行编程式导航
const router = useRouter();
const goBack = () => {
router.go(-1); // 后退一页
};
const goHome = () => {
router.push('/'); // 跳转到首页
// 或 router.push({ name: 'Home' });
};
</script>
<template>
<div>
<h1>用户 ID: {{ userId }}</h1>
<button @click="goBack">返回</button>
<button @click="goHome">回首页</button>
</div>
</template>
7. 状态管理:Pinia (Vue 官方推荐)
Pinia 是 Vue 的下一代状态管理库,比 Vuex 更简单、类型安全且支持组合式 API。
7.1 安装与创建 Store
pnpm add pinia
在 main.ts 中注册 Pinia:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
创建一个用户 Store:src/stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
// 定义用户类型
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
// 定义登录请求参数类型
interface LoginCredentials {
email: string;
password: string;
rememberMe?: boolean;
}
// 定义登录响应类型
interface LoginResponse {
user: User;
token: string;
expiresIn: number;
}
// 定义 Store
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null);
const token = ref<string>('');
// 初始化时从 localStorage 恢复 token
const initFromStorage = () => {
const storedToken = localStorage.getItem('auth_token');
const storedUser = localStorage.getItem('user_info');
if (storedToken) {
token.value = storedToken;
}
if (storedUser) {
try {
user.value = JSON.parse(storedUser);
} catch (error) {
console.error('Failed to parse stored user data:', error);
localStorage.removeItem('user_info');
}
}
};
// 调用初始化
initFromStorage();
// Getters (计算属性)
const isLoggedIn = computed(() => !!token.value);
const userName = computed(() => user.value?.name || '');
const userEmail = computed(() => user.value?.email || '');
const userAvatar = computed(() => user.value?.avatar || '/default-avatar.png');
// Actions (方法)
const login = async (credentials: LoginCredentials): Promise<LoginResponse> => {
try {
// 模拟 API 调用
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new Error('登录失败,请检查用户名和密码');
}
const data: LoginResponse = await response.json();
// 更新 state
user.value = data.user;
token.value = data.token;
// 持久化到 localStorage
if (credentials.rememberMe) {
localStorage.setItem('auth_token', data.token);
localStorage.setItem('user_info', JSON.stringify(data.user));
// 设置 token 过期时间(可选)
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + data.expiresIn);
localStorage.setItem('token_expires_at', expiresAt.toISOString());
} else {
// 仅保存在 sessionStorage(浏览器关闭后清除)
sessionStorage.setItem('auth_token', data.token);
sessionStorage.setItem('user_info', JSON.stringify(data.user));
}
return data;
} catch (error) {
console.error('登录错误:', error);
throw error;
}
};
const logout = (): void => {
// 清除 state
user.value = null;
token.value = '';
// 清除所有存储的认证信息
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
localStorage.removeItem('token_expires_at');
sessionStorage.removeItem('auth_token');
sessionStorage.removeItem('user_info');
// 可以在这里添加其他清理逻辑,如重定向到登录页
console.log('用户已登出');
};
const refreshToken = async (): Promise<string> => {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token.value}`,
},
});
if (!response.ok) {
throw new Error('Token 刷新失败');
}
const data = await response.json();
token.value = data.token;
// 更新存储的 token
if (localStorage.getItem('auth_token')) {
localStorage.setItem('auth_token', data.token);
} else if (sessionStorage.getItem('auth_token')) {
sessionStorage.setItem('auth_token', data.token);
}
return data.token;
} catch (error) {
console.error('Token 刷新错误:', error);
logout(); // 刷新失败时自动登出
throw error;
}
};
const updateProfile = async (profileData: Partial<User>): Promise<User> => {
try {
const response = await fetch('/api/user/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token.value}`,
},
body: JSON.stringify(profileData),
});
if (!response.ok) {
throw new Error('更新个人资料失败');
}
const updatedUser: User = await response.json();
user.value = updatedUser;
// 更新存储的用户信息
const storage = localStorage.getItem('user_info') ? localStorage : sessionStorage;
storage.setItem('user_info', JSON.stringify(updatedUser));
return updatedUser;
} catch (error) {
console.error('更新个人资料错误:', error);
throw error;
}
};
// 返回所有 state、getters 和 actions
return {
user,
token,
isLoggedIn,
userName,
userEmail,
userAvatar,
login,
logout,
refreshToken,
updateProfile,
};
});
在组件中使用 Store 的完整示例:src/components/LoginForm.vue
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
// 使用 Store
const userStore = useUserStore();
const router = useRouter();
// 表单数据
const email = ref<string>('');
const password = ref<string>('');
const rememberMe = ref<boolean>(false);
const isLoading = ref<boolean>(false);
const errorMessage = ref<string>('');
// 计算属性:表单验证
const isFormValid = computed(() => {
return email.value.trim() !== '' && password.value.trim() !== '';
});
// 登录处理
const handleLogin = async (): Promise<void> => {
if (!isFormValid.value) {
errorMessage.value = '请输入邮箱和密码';
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
const credentials = {
email: email.value,
password: password.value,
rememberMe: rememberMe.value,
};
await userStore.login(credentials);
// 登录成功后跳转到首页
router.push('/');
// 显示成功消息
console.log('登录成功,欢迎', userStore.userName);
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '登录失败,请重试';
} finally {
isLoading.value = false;
}
};
// 登出处理
const handleLogout = (): void => {
userStore.logout();
router.push('/login');
};
// 更新个人资料示例
const updateUserProfile = async (): Promise<void> => {
try {
await userStore.updateProfile({
name: '新的用户名',
avatar: 'https://example.com/new-avatar.jpg',
});
console.log('个人资料更新成功');
} catch (error) {
console.error('更新失败:', error);
}
};
</script>
<template>
<div class="login-container">
<!-- 登录表单 -->
<form v-if="!userStore.isLoggedIn" @submit.prevent="handleLogin">
<h2>用户登录</h2>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div class="form-group">
<label for="email">邮箱地址</label>
<input
id="email"
v-model="email"
type="email"
placeholder="请输入邮箱"
required
/>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
id="password"
v-model="password"
type="password"
placeholder="请输入密码"
required
/>
</div>
<div class="form-group checkbox">
<input
id="rememberMe"
v-model="rememberMe"
type="checkbox"
/>
<label for="rememberMe">记住我</label>
</div>
<button
type="submit"
:disabled="!isFormValid || isLoading"
class="login-button"
>
{{ isLoading ? '登录中...' : '登录' }}
</button>
</form>
<!-- 用户信息展示 -->
<div v-else class="user-info">
<h2>欢迎回来!</h2>
<div class="profile">
<img
:src="userStore.userAvatar"
:alt="userStore.userName"
class="avatar"
/>
<div class="details">
<h3>{{ userStore.userName }}</h3>
<p>{{ userStore.userEmail }}</p>
<p>Token: {{ userStore.token.substring(0, 20) }}...</p>
</div>
</div>
<div class="actions">
<button @click="updateUserProfile" class="btn-secondary">
更新资料
</button>
<button @click="handleLogout" class="btn-logout">
退出登录
</button>
</div>
</div>
</div>
</template>
<style scoped>
.login-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #fff;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input[type="email"],
.form-group input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-group.checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
}
.login-button {
width: 100%;
padding: 0.75rem;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.login-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.login-button:hover:not(:disabled) {
background-color: #3aa876;
}
.error-message {
padding: 0.75rem;
margin-bottom: 1rem;
background-color: #fee;
color: #c33;
border-radius: 4px;
border: 1px solid #fcc;
}
.user-info {
text-align: center;
}
.profile {
display: flex;
align-items: center;
gap: 1rem;
margin: 2rem 0;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.details h3 {
margin: 0;
color: #333;
}
.details p {
margin: 0.25rem 0;
color: #666;
}
.actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 2rem;
}
.btn-secondary,
.btn-logout {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary {
background-color: #f0f0f0;
color: #333;
}
.btn-secondary:hover {
background-color: #e0e0e0;
}
.btn-logout {
background-color: #ff6b6b;
color: white;
}
.btn-logout:hover {
background-color: #ff5252;
}
</style>
类型安全调用的关键点:
- Store 类型推断:Pinia 会自动推断
useUserStore()的返回类型,包含所有 state、getters 和 actions - 严格类型检查:所有函数参数和返回值都有明确的 TypeScript 类型定义
- 响应式更新:Store 中的
ref和computed在组件中自动保持响应式 - 持久化策略:根据用户选择使用
localStorage(长期存储)或sessionStorage(会话存储) - 错误处理:所有异步操作都有完整的 try-catch 错误处理
- 组件解耦:业务逻辑集中在 Store 中,组件只负责 UI 和用户交互
这个完整的 Store 实现提供了:
- 完整的登录/登出 action 方法
- Token 持久化逻辑(localStorage/sessionStorage)
- 用户信息管理
- Token 自动刷新机制
- 类型安全的组件调用示例
- 完整的样式和用户体验
8. HTTP 请求:Axios 与 TypeScript 封装
在实际项目中,前后端通信离不开 HTTP 请求。Axios 是目前最流行的 HTTP 库,结合 TypeScript 可以做到类型安全的 API 调用。
8.1 安装与基本配置
pnpm add axios
创建 src/utils/request.ts,封装一个带有拦截器的 Axios 实例:
import axios, {
type AxiosInstance,
type AxiosRequestConfig,
type InternalAxiosRequestConfig
} from 'axios';
import { ElMessage } from 'element-plus'; // 可选,需要先安装 element-plus
// 响应通用结构
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json;charset=UTF-8' }
});
// 请求拦截器:附加 token
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器:统一错误处理
request.interceptors.response.use(
(response) => {
const { data } = response;
if (data.code === 200) return data.data;
ElMessage.error(data.message || '请求失败');
return Promise.reject(new Error(data.message));
},
(error) => {
let message = '网络异常,请稍后重试';
if (error.response) {
const status = error.response.status;
if (status === 401) message = '登录已过期,请重新登录';
else if (status === 403) message = '没有权限';
else if (status === 404) message = '请求的资源不存在';
else if (status === 500) message = '服务器错误';
}
ElMessage.error(message);
return Promise.reject(error);
}
);
export default request;
8.2 类型安全的 API 接口层
创建 src/api/user.ts:
import request from '@/utils/request';
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
export interface LoginParams {
email: string;
password: string;
rememberMe?: boolean;
}
export interface LoginResult {
user: User;
token: string;
expiresIn: number;
}
export const userApi = {
login(params: LoginParams): Promise<LoginResult> {
return request.post('/auth/login', params);
},
getCurrentUser(): Promise<User> {
return request.get('/user/me');
},
updateProfile(data: Partial<User>): Promise<User> {
return request.put('/user/profile', data);
}
};
8.3 在组件中调用
<script setup lang="ts">
import { ref } from 'vue';
import { userApi, type User } from '@/api/user';
const currentUser = ref<User | null>(null);
const loading = ref(false);
const fetchUser = async () => {
try {
loading.value = true;
currentUser.value = await userApi.getCurrentUser();
} catch (error) {
console.error('获取用户信息失败:', error);
} finally {
loading.value = false;
}
};
</script>
9. UI 组件库集成:Element Plus
Element Plus 是 Vue 3 生态中最成熟的桌面端组件库,提供了丰富的 UI 组件和良好的 TypeScript 支持。
9.1 安装与全局引入
pnpm add element-plus @element-plus/icons-vue
在 main.ts 中注册:
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
const app = createApp(App);
app.use(ElementPlus);
app.mount('#app');
9.2 典型表单示例
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
interface UserForm {
name: string;
email: string;
age: number | null;
}
const formRef = ref<FormInstance>();
const formData = reactive<UserForm>({
name: '',
email: '',
age: null
});
const rules: FormRules<UserForm> = {
name: [{ required: true, message: '必填', trigger: 'blur' }],
email: [{ required: true, message: '必填', trigger: 'blur' },
{ type: 'email', message: '邮箱格式错误', trigger: 'blur' }],
age: [{ required: true, message: '必填', trigger: 'blur' },
{ type: 'number', min: 0, max: 150, message: '年龄不合理', trigger: 'blur' }]
};
const submit = async () => {
if (!formRef.value) return;
const valid = await formRef.value.validate().catch(() => false);
if (valid) {
ElMessage.success('提交成功');
// 调用 API
}
};
</script>
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" />
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age" :min="0" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">提交</el-button>
</el-form-item>
</el-form>
</template>
10. 测试:单元测试与 E2E 测试简介
10.1 单元测试(Vitest + Vue Test Utils)
如果项目创建时已选择 Vitest,可直接使用;否则安装:
pnpm add -D vitest @vue/test-utils jsdom
在 package.json 中添加脚本:
"scripts": {
"test": "vitest"
}
编写组件测试 src/components/__tests__/Counter.spec.ts:
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from '../Counter.vue';
describe('Counter.vue', () => {
it('点击按钮会增加计数', async () => {
const wrapper = mount(Counter);
const button = wrapper.find('button');
expect(wrapper.text()).toContain('0');
await button.trigger('click');
expect(wrapper.text()).toContain('1');
});
});
10.2 E2E 测试(Cypress)
Cypress 提供了强大的端到端测试能力:
pnpm add -D cypress
通过 npx cypress open 打开测试面板,可以编写模拟用户真实操作的脚本,比如登录流程、页面跳转等。
11. 项目构建与部署
11.1 生产环境构建
pnpm build
构建产物在 dist/ 目录,可部署到任何静态服务器,如 Nginx、Vercel、Netlify 等。
11.2 部署到 Vercel(免费)
- 将项目推送到 GitHub 仓库
- 访问 https://vercel.com 导入仓库
- Vercel 会自动检测 Vite 项目并配置构建命令
- 点击部署,即可获得一个公网可访问的地址
11.3 环境变量
在项目根目录创建 .env.production 和 .env.development,定义环境变量:
# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
在代码中通过 import.meta.env.VITE_API_BASE_URL 读取,构建时会被替换。
12. 总结与学习资源
恭喜你!通过这篇超过千行的实战教程,你已经系统掌握了 Vue 3 + TypeScript 全栈开发的核心技能:
- ✅ 使用 Vite 构建现代化 Vue 项目
- ✅ TypeScript 的基础类型、接口、泛型在 Vue 中的运用
- ✅ Vue 3 组合式 API:
ref、reactive、计算属性、侦听器、生命周期 - ✅ 组件通信:Props、Emits、Slots 的 TS 类型定义
- ✅ Vue Router 4 的路由配置、动态路由、导航守卫
- ✅ Pinia 状态管理的完整实践(Token 持久化、类型安全)
- ✅ Axios 拦截器封装与类型安全的 API 调用
- ✅ Element Plus 组件库的集成与使用
- ✅ 单元测试与 E2E 测试的基础配置
- ✅ 生产环境构建与部署
📚 推荐学习资源
- Vue 3 官方文档:https://cn.vuejs.org/
- TypeScript 中文手册:https://www.typescriptlang.org/zh/
- Vite 官方文档:https://cn.vitejs.dev/
- Pinia 官方文档:https://pinia.vuejs.org/zh/
- Vue Router 官方文档:https://router.vuejs.org/zh/
- Element Plus 官方文档:https://element-plus.org/zh-CN/
- Vitest 官方文档:https://cn.vitest.dev/
- Vue 3 技术揭秘(强烈推荐):https://ustbhuangyi.github.io/vue-analysis/
🚀 下一步行动
- 动手实践:依照本教程创建一个完整的“任务管理”或“博客系统”练手项目
- 阅读官方文档:深入每个工具的更多高级特性
- 加入社区:关注 Vue 官方微信公众号、参与 GitHub Discussions 和 Stack Overflow
- 持续学习:探索 VueUse 工具库、Nuxt.js 全栈框架、Serverless 部署等进阶主题
记住:学会一门技术最好的方式就是立即用它做一个小项目。如果你在实践过程中遇到任何问题,欢迎查阅文档或到 Vue Land 等社区提问。
前端开发的世界精彩纷呈,祝你在这条黄金赛道上越走越远!🎉
更多推荐


所有评论(0)