手把手教你实现一个vue3+ts+nodeJS后台管理系统(十六)

269 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情

前言

至此我们后端的用户、角色、权限模块就已经都完成了,感兴趣的小伙伴还可以去扩展实现更多的模块。那么现在就要开始实现此系统的前端部分。

前端系统设计

前端部分主要分为三个模块,一是登录模块,二是layout页面布局模块,三是管理页面模块。登录模块的表单包含用户名密码及图形验证码防止恶意登录,layout页面布局模块包含页头、侧边栏(显示用户的权限菜单),管理页面模块即点击权限菜单所展示的内容。layout及管理页面模块如下所示:

image.png

确认完系统所需模块后,该系统主要逻辑的流程大致如下:登录后根据用户名判断登录用户的角色权限,然后跳转获取登录用户的信息并渲染出该用户角色所对应的权限页面,且该用户只能在他的权限页面上看到它的角色所拥有的权限按钮,其余按钮皆不可见。在权限页面实现对应的增删改查功能,还有添加个人中心以便用户对个人信息进行修改。

设置代理解决跨域

但完成上述的前提是需要后端接口。所以我们首先先看一下接口文档,确定需要的接口及后端服务信息。

image.png

由于文章篇幅原因,只能展示一些,详见我系统的接口文档vue3+ts+nodeJs后台管理系统接口文档

然后由于端口号的不同,涉及到跨域,所以我们通过代理解决跨域的问题。

vite.config.ts

export default defineConfig({
  ...
  server: {
    // 系统端口号
    port: 8080,
    // 是否自动打开浏览器
    open: true,
    // 代理跨域
    proxy: {
      '/user': {
        target: 'http://127.0.0.1:9999',
        changeOrigin: true,
        rewrite: (path) => path.replace('//user$/', '')
      }
    }
  }
})

配置axios

首先我们需要知道,登录后请求过来的token是需要存储在客户端的,然后每次请求时都要带上token。本系统选择将token存储在cookie中,所以我们添加获取、设置token的配置文件

先安装js-cookie插件便于对cookie的操作

npm i js-cookie@3.0.1

utils/auth.ts

import Cookies from 'js-cookie';
// 设置token键值
const TokenKey = 'Access-Token';
/**
 * 获取token
 */
export function getToken() {
  return Cookies.get(TokenKey);
}
/**
 * 设置token
 * @param token
 */
export function setToken(token: string) {
  return Cookies.set(TokenKey, token);
}
/**
 * 移除token
 */
export function removeToken() {
  return Cookies.remove(TokenKey);
}
// refreshToken键值
const RefreshTokenKey = 'Refresh-Token';
​
export function getRefreshToken() {
  return Cookies.get(RefreshTokenKey);
}
​
export function setRefreshToken(token: string) {
  return Cookies.set(RefreshTokenKey, token, { expires: 7 });
}
​
export function removeRefreshToken() {
  return Cookies.remove(RefreshTokenKey);
}
​

然后我们在axios的配置文件拦截器中存在请求拦截和响应拦截

  • 请求拦截时,我们对需要token请求头的接口带上请求头
  • 响应拦截时,先判断是否为二进制文件blob类型,不是则判断code是否为0(接口请求成功),否则返回响应失败信息,是则返回数据。但响应拦截时,后端可能发现token过期,而此时又该返回信息,所以我们应该调用刷新token的接口,并进行完之前的请求。除非refreshToken也过期,我们才跳转登录页重新登录(当然我们路由还没有写)

utils/http.ts

import Axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import router from '../router/index.js';
import { getToken, setToken, removeToken, setRefreshToken, removeRefreshToken, getRefreshToken } from '../utils/auth';
import { ElMessage } from 'element-plus';
const BASE_URL = ''; //请求接口url 如果不配置 则默认访问链接地址
const TIME_OUT = 20000; // 接口超时时间// 是否正在刷新的标记
let isRefreshing = false;
// 重试队列,每一项将是一个待执行的函数形式
let requests: any[] = [];
​
const instance = Axios.create({
  baseURL: BASE_URL,
  timeout: TIME_OUT
});
// 不需要token的接口白名单
const whiteList = ['/user/login', '/user/refreshToken', '/user/checkCode'];
​
// 添加请求拦截器
instance.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    console.log(config);
​
    if (!whiteList.includes(config.url)) {
      let Token = getToken();
      if (Token && Token.length > 0) {
        config.headers['Authorization'] = Token;
      }
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);
​
// 添加响应拦截器
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    // 如果返回的类型为二进制文件类型
    if (response.config.responseType === 'blob') {
      if (response.status != 200) {
        ElMessage.error('请求失败' + response.status);
        return Promise.reject();
      } else if (!response.headers['content-disposition']) {
        ElMessage.error('暂无接口访问权限');
        return Promise.reject();
      }
      return response;
    } else {
      // 如果接口请求失败
      if (response.data.code !== 0) {
        let errMsg = response.data.message || '系统错误';
        // token过期
        if (response.data.code == 401) {
          const config = response.config;
          // token失效,判断请求状态
          if (!isRefreshing) {
            isRefreshing = true;
            // 刷新token
            return instance({
              url: 'http://127.0.0.1:9999/user/refreshToken',
              method: 'post',
              data: { refreshToken: getRefreshToken() }
            })
              .then((res) => {
                // 刷新token成功,更新最新token
                const { token, refreshToken } = res.data;
                setToken(token);
                setRefreshToken(refreshToken);
                //已经刷新了token,将所有队列中的请求进行重试
                requests.forEach((cb) => cb(token));
                // 重试完了别忘了清空这个队列
                requests = [];
                return instance(config);
              })
              .catch(() => {
                removeToken();
                removeRefreshToken();
                // 重置token失败,跳转登录页
                router.replace({
                  path: '/login',
                  query: {
                    redirect: router.currentRoute.fullPath //登录成功后跳入浏览的当前页
                  }
                });
              })
              .finally(() => {
                isRefreshing = false;
              });
          } else {
            // 返回未执行 resolve 的 Promise
            return new Promise((resolve) => {
              // 用函数形式将 resolve 存入,等待刷新后再执行
              requests.push(() => {
                resolve(instance(config));
              });
            });
          }
        } else {
          ElMessage.error(errMsg);
        }
        return Promise.reject(errMsg);
      }
      return response.data;
    }
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  }
);
​
export default instance;
​