当前位置: 首页 > news >正文

Vue3项目实战:从0到1开发企业级中后台系统(3):架构核心!手把手封装Axios、Pinia、Router

专栏介绍

  • “本专栏将手把手带你实战一个标准的企业级中后台管理系统,你学到的不是零散的知识点,而是一套完整的、可复用的开发流程和解决方案。”
  • “从项目搭建、架构设计到核心业务模块开发,我会分享每一步的最佳实践和十年总结的避坑心得。更重要的是,在专栏最后,我会教你如何从‘程序员’思维转变为‘工程师’思维,抽象封装属于自己的业务组件,这才是你职场进阶的关键。”
  • “无论你是想深入学习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()}`; // 后期这里可以统一处理tokenreturn 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: numbernickName: stringuserName: stringphone: stringtype: number[key:string]: any
}
interface UserState {accessToken: string | nullrefreshToken: string | nulluser: 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.accessTokenthis.refreshToken = res.refreshTokenlocalStorage.setItem('accessToken', res.accessToken)localStorage.setItem('refreshToken', res.refreshToken)this.storeGetUserInfo()// return res})},storeUserLoginPhone (data) {return userLoginPhone(data).then((res: any) => {this.accessToken = res.accessTokenthis.refreshToken = res.refreshTokenlocalStorage.setItem('accessToken', res.accessToken)localStorage.setItem('refreshToken', res.refreshToken)this.storeGetUserInfo()// return res})},storeUserRegistry (data) {return userRegistry(data).then((res: any) => {this.accessToken = res.accessTokenthis.refreshToken = res.refreshToken})},storeGetUserInfo () {return getUserInfo().then((res: any) => {this.user = reslocalStorage.setItem('user', JSON.stringify(res))})},storerefreshToken () {return refreshToken().then((res: any) => {this.accessToken = res.access_tokenthis.refreshToken = res.refresh_token})},storeLogout () {this.accessToken = nullthis.refreshToken = nullthis.user = nulllocalStorage.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();// 除白名单外 其余接口需要tokenconst 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;

总结

本篇我们完成了

  1. Axios封装 - 项目的"外交系统",负责与服务器通信
  2. 路由封装 - 项目的"导航系统",管理页面跳转和权限
  3. Pinia封装 - 项目的"记忆系统",管理全局数据和状态

这三大支柱就像房子的承重墙,奠定了整个项目的稳定架构。现在我们的项目已经具备了企业级应用的基础架构能力!

记得动手实践一下哦,光看不练假把式!遇到问题欢迎在评论区留言或文章末尾右下角联系哦!备注"csdn"免费答疑~

http://www.dtcms.com/a/573330.html

相关文章:

  • Spark on Yarn安装部署
  • 建设系统网站怎么做自然优化
  • 国产化数据库选型深度剖析:金仓KES与达梦DM全生命周期成本对比
  • Java 读取 Word 文本框中的文本和图片:Spire.Doc for Java 实践指南
  • 网站建设开发定制微信网站如何做
  • 商城项目业务总结
  • 安卓16提前发布能否改写移动生态格局
  • JVM :内存、性能调优与 JIT
  • JVM问题排查流程
  • 仲恺做网站wordpress屏蔽功能org
  • AI视频创作工具汇总:MoneyPrinterTurbo、KrillinAI、NarratoAI、ViMax
  • 部署我的世界-java版服务器-frp内网穿透
  • Eureka 注册中心原理与服务注册发现机制
  • Unity使用RVM实现实时人物视频抠像(无绿幕)
  • 物联网传感器环境自适应校准与精度补偿技术
  • 【低空安全】低空安防威胁与挑战
  • 微网站建设包括哪些iis5.1怎么新建网站
  • 45_AI智能体核心业务之Agent决策流程管理器:构建智能对话系统的工作流引擎
  • wordpress api定制济南seo公司案例
  • vscode运行ipynb文件:使用docker中的虚拟环境
  • 网站布局有哪些企业网站源码怎么获取
  • 如何科学地对单片机进行AI性能测试:指标、方法与实战
  • 软件设计师-树-叶子结点个数
  • 饭店网站模板北京网站制作郑州
  • 小型企业网站建设公司株洲网上房地产
  • 数据中台的核心功能包含哪些?基本思路建设思路是什么?
  • 牛网网站建设网站内容优化关键词布局
  • C++进阶:(五)map系列容器的全面解析
  • C++之lambda表达式使用解读
  • kanass零基础学习,如何做好测试管理