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()}`; // 后期这里可以统一处理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;

总结

本篇我们完成了

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sherry Tian

打赏1元鼓励作者

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值