专栏介绍
- “本专栏将手把手带你实战一个标准的企业级中后台管理系统,你学到的不是零散的知识点,而是一套完整的、可复用的开发流程和解决方案。”
- “从项目搭建、架构设计到核心业务模块开发,我会分享每一步的最佳实践和十年总结的避坑心得。更重要的是,在专栏最后,我会教你如何从‘程序员’思维转变为‘工程师’思维,抽象封装属于自己的业务组件,这才是你职场进阶的关键。”
- “无论你是想深入学习Vue3生态,还是渴望在工作中独立承担项目,这个专栏都将为你提供巨大的帮助。”
技术选型
要打造一个企业级项目,选择合适的“装备”至关重要。我们的选择是:
- 构建工具: Vite (进行项目构建)
- 语言:TypeScript (开发语言)
- 样式: less (编写样式 )
- 状态管理: pinia(vuex的进化版,更简单,更友好)
- 路由:vue-router(单页应用的导航系统,不可或缺)
- HTTP客户端:axios (数据请求)
- 代码规范: CommitLint + ESLint + StyleLint + Prettier + LintStage 进行团队项目规范
- UI库: ElementPlus 组件库(由饿了么技术团队开发)
封装axios
/src/utils/status.ts
我们先来创建一个status.ts文件,文件路径 /src/utils/status.ts ,我们一会儿会用到它,在这个文件中我们封装一个getMessageInfo 方法,用来集中处理错误信息。我们会频繁使用这些信息,将它封装起来统一调度。内容如下:
export const getMessageInfo = (status: number | string): string => {
let msg = "";
switch (status) {
case 400:
msg = "请求错误(400)"; // 请求语法错误,服务器无法解析(如 JSON 格式错误)
break;
case 403:
msg = "拒绝访问(403)"; // 用户已认证,但无权访问该资源(才是“没权限”)
break;
case 401:
msg = "未授权(401)"; // 用户未登录或认证失败(注意:不是“没权限”)
break;
case 500:
msg = "服务器错误(500)"; // 服务器内部错误,未处理的异常
break;
case 503:
msg = "服务不可用(503)"; // 服务器暂时过载或维护,无法处理请求
break;
default:
msg = `连接出错(${status})!`; // 其他未知错误
}
return msg;
};
状态码分类
这里不想看的,可以调过,直接看request,不懂的再回来看。
- 1xx - 信息性状态码:表示请求已被接收,需要继续处理。
- 2xx - 成功状态码:表示客户端请求成功。
- 3xx - 重定向状态码:表示客户端需要采取进一步操作才能完成请求,通常是跳转。
- 4xx - 客户端错误状态码:请求包含语法错误或无法完成,错误在客户端,比如请求资源不存在或格式不对。
- 5xx - 服务端错误状态码:服务器在处理请求时出错。
常见状态码
- 200 - 成功:请求成功,响应体包含请求结果。
- 201 - 新建成功:用于 POST/PUT 请求,表示资源创建成功。
- 204 - 无内容:请求成功,但响应体为空(如 DELETE 成功)。
- 301 - 永久重定向:资源已永久迁移,浏览器会缓存该跳转。
- 302 - 临时重定向:资源临时迁移,浏览器自动跳转,不缓存。
- 304 - 资源未被修改:用于缓存协商(配合 If-None-Match 或 If-Modified-Since),节省带宽。
- 400 - 请求错误:请求语法错误,服务器无法解析(如 JSON 格式错误)。
- 401 - 未认证:用户未登录或认证失败(注意:不是“没权限”)。
- 403 - 禁止访问:用户已认证,但无权访问该资源(才是“没权限”)。
- 404 - 资源未找到:服务器找不到请求的资源(接口错误,检查接口路径是否正确)。
- 405 - 方法不允许:请求方法(如 POST)不被允许用于该资源。
- 429 - 请求过多:限流场景,客户端发送请求过于频繁。
- 500 - 服务器错误:服务器内部错误,未处理的异常。
- 502 - 网关错误:服务器作为网关或代理时,从上游服务器收到无效响应。
- 503 - 服务不可用:服务器暂时过载或维护,无法处理请求。
- 504 - 网关超时:服务器作为网关或代理时,未能及时从上游服务器获得响应。
/src/utils/request.ts
我们创建一个request.ts文件,文件路径 /src/utils/request.ts ,在这个文件中我们对响应码进行判断并给出提示。内容如下:
// 导入依赖
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { getMessageInfo } from './status';
import { ElMessage } from 'element-plus';
// 定义响应数据接口, 假设后端返回的数据格式是统一的
interface BaseResponse<T = any> {
code: number | string;
message: string;
data: T;
}
// 创建 axios 实例 service
const service = axios.create({
// 从 Vite 环境变量中读取 API 基地址(如 `/api` 或 `https://example.com/api`)
baseURL: import.meta.env.VITE_APP_API,
// 请求超时设置,防止页面卡死
timeout: 15000
});
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// config.headers.Authorization = `Bearer ${getToken()}`; // 后期这里可以统一处理token
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status >= 200 && response.status < 300) {
return response;
}
ElMessage({
message: getMessageInfo(response.status),
type: 'error'
});
return response;
},
// 请求失败
(error: any) => {
const { response } = error;
// 如果 response 存在(说明请求发出去了,但服务器返回错误)
if (response) {
// 请求已发出,但是不在2xx的范围
ElMessage({
message: getMessageInfo(response.status),
type: 'error'
});
// 将错误继续抛出 这里返回后,后面的代码将不会执行
return Promise.reject(response.data);
}
// 如果 response 不存在(网络异常、DNS 失败、断网)
ElMessage({
message: '网络异常,请稍后再试!',
type: 'error'
});
}
);
// 自定义请求函数 requestInstance(二次处理响应)
const requestInstance = <T = any>(config: AxiosRequestConfig): Promise<T> => {
const conf = config;
return new Promise((resolve, reject) => {
service.request<any, AxiosResponse<BaseResponse>>(conf).then((res: AxiosResponse<BaseResponse>) => {
const data = res.data;
// 用于处理业务层面的成功/失败
if (data.code !== 0) {
ElMessage({
message: data.message,
type: 'error'
});
// 自定义错误对象,便于捕获更多信息
reject(data);
} else {
ElMessage({
message: data.message,
type: 'success'
});
resolve(data.data as T);
}
});
});
};
// 封装 get/post/put/del 方法
export function get<T = any, U = any>(config: AxiosRequestConfig, url: string, params?: U): Promise<T> {
return requestInstance({ ...config, url, method: 'GET', params: params });
}
export function post<T = any, U = any>(
config: AxiosRequestConfig,
url: string,
data: U
): Promise<T> {
return requestInstance({ ...config, url, method: 'POST', data: data });
}
export function put<T = any, U = any>(
config: AxiosRequestConfig,
url: string,
params?: U
): Promise<T> {
return requestInstance({ ...config, url, method: 'PUT', params: params });
}
export function del<T = any, U = any>(
config: AxiosRequestConfig,
url: string,
data: U
): Promise<T> {
return requestInstance({ ...config, url, method: 'DELETE', data: data });
}
// 导出默认实例和具名方法
export default service;
axios:核心 HTTP 客户端。AxiosError:错误类型,用于错误处理。AxiosRequestConfig/InternalAxiosRequestConfig:请求配置类型(后者包含内部字段)。AxiosResponse:响应类型。getMessageInfo:自定义函数,根据状态码返回友好提示信息(如 404 → “资源未找到”)。ElMessage:Element Plus 的消息提示组件,用于弹出提示。
/src/api
在/src/api这个文件夹下对接口进行分类管理,将不同模块的接口放在一起。在/src/api下建立不同的文件夹代表不同类模块的API,在index.ts中编写接口配置,在types.ts中编写接口所需的请求参数类型以及响应类型。
以user模块为例,我们将建立下面两个文件。
/src/api/user/types.ts
// 登录所需的参数
export type LoginRequest = {
username: string;
password: string;
};
// 刷新登录信息需要的参数
export type reLoginRequest = {
accessToken: string;
};
// 登录后返回的响应信息
export type LoginResponse = {
username: string;
roles: Array<string>;
accessToken: string;
};
/src/api/user/index.ts
import { post } from '@/utils/request';
// 导入类型
import { LoginRequest, LoginResponse, reLoginRequest } from '@/api/user/types';
// post 请求直接传入一个 data 即可 url 我们直接在此处封装好
// 需要更改时也只需在此处更改
export const userLogin = async (data?: LoginRequest) => {
return post<LoginResponse>({}, '/login', data);
};
export const refreshUserInfo = async (data?: reLoginRequest) => {
return post<LoginResponse>({}, '/getUserInfo', data);
};
使用的时候我们可以直接在组件中引用,也可将其封装在store的action中,将相关的store与接口关联起来。
在组件中调用
import { userLogin } from "@/api/user";
const submit = () => {
const params = {
userName: 'admin',
password: '123456'
}
userLogin(params).then(res => {
console.log('登录成功')
})
}
封装Pinia
/src/store/user.ts
当数据需要在整个系统中使用的时候,我们可以将它放到store中存储,这样不用每次都调接口,用的时候直接从store中取就行了。
import { defineStore } from "pinia";
import { userLogin, userLoginPhone, getUserInfo, refreshToken, userRegistry } from "@/api/user";
interface IUserInfo {
userId: number
nickName: string
userName: string
phone: string
type: number
[key:string]: any
}
interface UserState {
accessToken: string | null
refreshToken: string | null
user: IUserInfo | null
}
export const useUserStore = defineStore('userInfo', {
state: (): UserState => {
const storedUser = localStorage.getItem('user')
return {
accessToken: localStorage.getItem('accessToken') || null,
refreshToken: localStorage.getItem('refreshToken') || null,
user: storedUser ? JSON.parse(storedUser) : null // ← 解析成对象
}
},
getters: {},
actions: {
storeUserLogin (data) {
return userLogin(data).then((res: any) => {
this.accessToken = res.accessToken
this.refreshToken = res.refreshToken
localStorage.setItem('accessToken', res.accessToken)
localStorage.setItem('refreshToken', res.refreshToken)
this.storeGetUserInfo()
// return res
})
},
storeUserLoginPhone (data) {
return userLoginPhone(data).then((res: any) => {
this.accessToken = res.accessToken
this.refreshToken = res.refreshToken
localStorage.setItem('accessToken', res.accessToken)
localStorage.setItem('refreshToken', res.refreshToken)
this.storeGetUserInfo()
// return res
})
},
storeUserRegistry (data) {
return userRegistry(data).then((res: any) => {
this.accessToken = res.accessToken
this.refreshToken = res.refreshToken
})
},
storeGetUserInfo () {
return getUserInfo().then((res: any) => {
this.user = res
localStorage.setItem('user', JSON.stringify(res))
})
},
storerefreshToken () {
return refreshToken().then((res: any) => {
this.accessToken = res.access_token
this.refreshToken = res.refresh_token
})
},
storeLogout () {
this.accessToken = null
this.refreshToken = null
this.user = null
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
localStorage.removeItem('user')
}
},
persist: true // 开启持久化
});
/src/store/index.ts
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
// 使用pinia数据持久化插件
pinia.use(piniaPluginPersistedstate);
export default pinia;
在组件中使用store数据
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const { user } = storeToRefs(userStore);
// <span v-if="!user"><a @click="gotoLogin">登录</a>/<a @click="gotoRegistry">注册</a></span> 在模板中直接使用user
// 在js中需要用user.value
watch(
() => user.value,
(newUser) => {
console.log('user changed:', newUser)
if (newUser) {
loadChartUrl()
}
},
{ immediate: true }
)
封装路由router
模块化路由管理 - 让导航更有条理
与其把所有路由堆在一个文件里(就像把整个城市的道路都画在一张纸上),不如按模块分开:
创建登录页面的路由配置 (/src/router/modules/login.ts):
import { RouteRecordRaw } from 'vue-router';
export default {
path: '/login', // 路径:就像街道地址
name: 'LoginPage', // 路由名称:就像门牌号
component: () => import('@/views/login/index.vue'), // 对应的页面组件
meta: {
role: ['common', 'admin'], // 元信息:比如哪些角色可以访问
},
children: [], // 子路由:就像这条街上的小巷子
} as RouteRecordRaw;
在主路由文件中统一导入 (/src/router/index.ts):
import { createRouter, createWebHashHistory } from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
// 自动导入所有模块路由(就像把各个区的地图合并成全市地图)
const modules = import.meta.glob('./modules/*.ts', { eager: true });
const routeModuleList: RouteRecordRaw[] = [];
Object.keys(modules).forEach((key) => {
const mod = (modules[key] as any).default || {};
routeModuleList.push(mod);
});
const router = createRouter({
history: createWebHashHistory(),
routes: routeModuleList
});
const whiteList = ['/login', '/registry', '/', '/home', '/news', '/history', '/relate'];
router.beforeEach(async (_to, _from, next) => {
NProgress.start();
// 除白名单外 其余接口需要token
const token = localStorage.getItem('accessToken')
if (token && token !== 'undefined') {
// 如果已经登陆且当前是login页面 跳到登入页
if (_to.path === '/login') {
next({
path: '/'
})
} else {
next()
}
} else {
// 白名单内
if (whiteList.some(route => _to.path === route || _to.path.startsWith('/home/detail/'))) {
next()
} else {
try {
await ElMessageBox.confirm(
'您还没有登录,请先登录!',
'提示',
{
confirmButtonText: '去登录',
cancelButtonText: '取消',
type: 'warning'
}
);
// 用户点击了“确认”按钮
next({
path: '/login',
query: {
redirect: _to.fullPath
}
});
} catch (error) {
// 用户点击了“取消”按钮
ElMessage({
message: '已取消',
type: 'info'
});
if (_from.path !== '/home') {
next({
path: '/'
})
} else {
next(false)
}
}
}
}
});
router.afterEach((_to) => {
NProgress.done();
});
export default router;
总结
本篇我们完成了
- Axios封装 - 项目的"外交系统",负责与服务器通信
- 路由封装 - 项目的"导航系统",管理页面跳转和权限
- Pinia封装 - 项目的"记忆系统",管理全局数据和状态
这三大支柱就像房子的承重墙,奠定了整个项目的稳定架构。现在我们的项目已经具备了企业级应用的基础架构能力!
记得动手实践一下哦,光看不练假把式!遇到问题欢迎在评论区留言或文章末尾右下角联系哦!备注"csdn"免费答疑~
69

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



