Vue3 + TypeScript 实战:从零构建一个电商后台管理系统
最近几年,前端开发领域的技术栈迭代速度令人目不暇接,但 Vue 3 配合 TypeScript 的组合,却稳稳地站住了脚跟,成为了许多中大型项目,尤其是后台管理系统的首选方案。我身边不少从 Vue 2 迁移过来的朋友,最初都对组合式 API 和严格的类型检查感到些许不适应,但真正用上手之后,普遍反馈是“回不去了”——代码的组织性、可维护性和开发体验都有了质的飞跃。如果你已经掌握了 Vue 的基础语法,正跃跃欲试想挑战一个完整的实战项目,那么一个功能完备的电商后台管理系统,无疑是最佳的练手场。它几乎涵盖了现代前端开发的所有核心环节:项目工程化、路由与权限、状态管理、组件封装、接口联调以及性能优化。本文将带你从零开始,手把手搭建这样一个系统,我会分享许多在官方文档里找不到的实战细节和“踩坑”经验。
1. 项目初始化与工程化配置
万事开头难,一个良好的项目开端能避免后续无数麻烦。我们不再使用传统的 Vue CLI,而是选择更快的 Vite 作为构建工具。它不仅启动和热更新速度极快,而且对 TypeScript 的支持是原生的,开箱即用。
首先,打开你的终端,执行以下命令来创建项目:
npm create vue@latest vue3-ts-admin
创建过程中,命令行会交互式地让你选择需要的功能。请务必勾选上 TypeScript、Vue Router、Pinia、ESLint 和 Prettier。至于测试工具,可以根据需要选择 Vitest,这对于我们当前的项目来说是可选的。
项目创建完成后,先别急着写代码。有几项关键的配置需要调整,以确保团队协作和代码质量。
首先,是 TypeScript 的配置 (tsconfig.json)。Vite 生成的默认配置通常够用,但我建议增加一些严格的类型检查选项,这能帮助我们在开发阶段就捕获潜在的错误:
{
"compilerOptions": {
// ... 其他配置
"strict": true,
"noImplicitAny": true, // 禁止隐式的 any 类型
"strictNullChecks": true, // 严格的 null 检查
"types": ["vite/client"] // 确保 Vite 客户端类型被识别
}
}
其次,是 ESLint 和 Prettier 的整合。这两者一个负责代码质量,一个负责代码格式,搭配使用能保证代码风格统一。安装必要的依赖:
npm install -D eslint-plugin-prettier eslint-config-prettier
然后,在 .eslintrc.cjs 配置文件中,将 prettier 的规则集成进去,避免两者冲突:
module.exports = {
// ... 其他配置
extends: [
// ... 其他扩展
'plugin:prettier/recommended' // 放在最后,用 Prettier 的规则覆盖格式相关的 ESLint 规则
]
}
最后,在 package.json 中添加几个实用的脚本,方便日常开发:
{
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build", // 先进行类型检查,再构建
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix", // 自动修复可修复的 lint 错误
"format": "prettier --write ." // 格式化所有文件
}
}
注意:强烈建议在提交代码前运行
npm run lint和npm run format,或者配置 Git Hooks(如 husky)来自动执行,这是保证代码库整洁的最低成本方式。
2. 核心架构:路由、状态管理与请求封装
一个后台管理系统的骨架,主要由路由、状态管理和网络请求这三部分构成。搭建好这个骨架,后续的功能开发就像往里面填充血肉,会顺畅很多。
2.1 基于角色权限的动态路由设计
电商后台通常涉及多个角色,如超级管理员、商品管理员、订单管理员等。不同角色能访问的页面(路由)和操作权限是不同的。静态地在 router/index.ts 里定义所有路由,然后通过导航守卫来隐藏菜单,并不是最佳实践,因为用户依然可以通过输入 URL 直接访问。更安全的做法是动态路由:用户登录后,根据其角色从后端获取有权限的路由配置,再动态添加到路由实例中。
首先,我们定义路由的元信息类型,用于携带权限数据:
// types/router.d.ts
import { RouteRecordRaw } from 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
title: string; // 页面标题,用于显示在浏览器标签和面包屑
requiresAuth?: boolean; // 是否需要登录
roles?: string[]; // 允许访问的角色数组,如 ['admin', 'editor']
icon?: string; // 菜单图标,例如 'el-icon-s-goods'
hidden?: boolean; // 是否不在侧边栏菜单中显示
keepAlive?: boolean; // 是否缓存该页面组件
}
}
export type AppRouteRecordRaw = RouteRecordRaw & {
meta?: RouteMeta;
children?: AppRouteRecordRaw[];
};
接着,我们创建两套路由表。一套是静态路由,比如登录页、404页面,这些是所有用户都能访问的。另一套是动态路由,即需要权限才能访问的业务模块路由。
// router/routes.ts
import type { AppRouteRecordRaw } from '@/types/router';
// 静态路由(无需权限)
export const constantRoutes: AppRouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录', hidden: true } // hidden 为 true 不会出现在菜单
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/error-page/404.vue'),
meta: { title: '404', hidden: true }
}
];
// 动态路由(需要权限),这里先本地模拟,实际应由后端返回
export const asyncRoutes: AppRouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/BasicLayout.vue'),
redirect: '/dashboard',
meta: { title: '首页' },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '仪表盘', icon: 'dashboard', requiresAuth: true }
},
{
path: 'product',
name: 'Product',
redirect: '/product/list',
meta: { title: '商品管理', icon: 'shopping', roles: ['admin', 'product_manager'] },
children: [
{
path: 'list',
name: 'ProductList',
component: () => import('@/views/product/list/index.vue'),
meta: { title: '商品列表', keepAlive: true }
},
{
path: 'category',
name: 'ProductCategory',
component: () => import('@/views/product/category/index.vue'),
meta: { title: '商品分类' }
}
]
}
// ... 更多路由
]
}
];
在路由守卫中,我们实现动态添加路由的逻辑:
// router/permission.ts
import router from './index';
import { useUserStore } from '@/stores/user';
import { usePermissionStore } from '@/stores/permission';
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const permissionStore = usePermissionStore();
// 1. 判断是否需要登录
if (to.meta.requiresAuth && !userStore.token) {
next(`/login?redirect=${to.fullPath}`);
return;
}
// 2. 如果已登录,且是首次进入,则获取用户信息和权限路由
if (userStore.token && !permissionStore.isRoutesAdded) {
try {
// 获取用户信息(包含角色)
await userStore.getUserInfo();
// 根据角色,过滤出有权限的动态路由
const accessRoutes = await permissionStore.generateRoutes(userStore.roles);
// 动态添加到路由实例
accessRoutes.forEach(route => router.addRoute(route));
permissionStore.setRoutesAdded(true);
// 添加完路由后,重定向到目标页面,确保路由生效
next({ ...to, replace: true });
} catch (error) {
// 获取失败,清空token,跳转到登录页
userStore.resetToken();
next(`/login?redirect=${to.path}`);
}
} else {
next();
}
});
2.2 使用 Pinia 进行现代化状态管理
Vuex 4 虽然能用,但 Pinia 才是 Vue 3 的“亲儿子”。它更简洁,完美支持组合式 API 和 TypeScript,并且没有 mutations 的概念,直接修改 state 或者使用 actions 都可以。
我们以管理用户信息和权限的状态为例。首先安装 Pinia:
npm install pinia
然后创建一个用户状态存储:
// stores/user.ts
import { defineStore } from 'pinia';
import { login, getUserInfo, logout } from '@/api/user';
import type { LoginForm, UserInfo } from '@/api/user/types';
interface UserState {
token: string | null;
userInfo: Partial<UserInfo> | null;
roles: string[];
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: localStorage.getItem('token'),
userInfo: null,
roles: []
}),
getters: {
isLoggedIn: state => !!state.token,
userName: state => state.userInfo?.name || ''
},
actions: {
async login(loginForm: LoginForm) {
try {
const { data } = await login(loginForm);
this.token = data.token;
localStorage.setItem('token', data.token);
// 登录成功后,通常需要获取用户信息
await this.getUserInfo();
} catch (error) {
// 统一在调用处处理错误,这里可以清理状态
this.resetToken();
throw error; // 重新抛出错误
}
},
async getUserInfo() {
if (!this.token) return;
const { data } = await getUserInfo();
this.userInfo = data;
this.roles = data.roles || [];
},
async logo

&spm=1001.2101.3001.5002&articleId=152720915&d=1&t=3&u=69368bc65bc146e89ef18282b23f56cb)
369

被折叠的 条评论
为什么被折叠?



