Vue.js + Vuex + TypeScript实战项目开发与项目优化

1,133 阅读11分钟

内容输出来源:拉勾教育大前端高薪训练营

一、创建项目

1. 使用@vue/cli构建项目

安装Vue Cli

npm i -g @vue/cli

创建项目

vue create edu-boss-fed

进入项目目录

cd edu-boss-fed

启动开发服务

npm run serve

启动成功,根据提示访问给出的服务地址

App running at:
- Local: http://localhost:8080/
- Network: http://10.10.100.145:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.

看到该页面,则项目创建成功!

2、加入Git版本管理

  1. 创建远程仓库
  2. 将本地仓库推到线上 如果没有本地仓库:
# 创建本地仓库,cd 到本地项目目录下
git init

# 将文件添加到暂存区
git add 文件
git add . # 首次提交

# 将暂存区的文件提交到本地仓库
git commit "提交日志"

# 添加远端仓库地址
git remote add origin 你的远程仓库地址

# 推送提交
git push -u origin master

如果已有本地仓库:

# 添加远端仓库地址
git remote add origin 你的远程仓库地址

# 推送提交
git push -u origin master

3. 初始目录结构说明

1 . 
2 ├── node_modules # 第三⽅包存储⽬录 
3 ├── public # 静态资源⽬录,任何放置在 public ⽂件夹的静态资源都会被简单的复 制,⽽不经过 webpack 
4 │ ├── favicon.ico 
5 │ └── index.html 
6 ├── src 
7 │ ├── assets # 公共资源⽬录,放图⽚等资源 
8 │ ├── components # 公共组件⽬录 
9 │ ├── router # 路由相关模块 
10 │ ├── store # 容器相关模块 
11 │ ├── views # 路由⻚⾯组件存储⽬录 
12 │ ├── App.vue # 根组件,最终被替换渲染到 index.html ⻚⾯中 #app ⼊⼝ 节点 
13 │ ├── main.ts # 整个项⽬的启动⼊⼝模块 14 │ ├── shims-tsx.d.ts # ⽀持以 .tsc 结尾的⽂件,在 Vue 项⽬中编写 jsx 代码 
15 │ └── shims-vue.d.ts # 让 TypeScript 识别 .vue 模块 
16 ├── .browserslistrc # 指定了项⽬的⽬标浏览器的范围。这个值会被 @babel/pre set-env 和 Autoprefixer ⽤来确定需要转译的 JavaScript 特性和需要添加的 C SS 浏览器前缀 
17 ├── .editorconfig # EditorConfig 帮助开发⼈员定义和维护跨编辑器(或IDE) 的统⼀的代码⻛格 
18 ├── .eslintrc.js # ESLint 的配置⽂件 19 ├── .gitignore # Git 的忽略配置⽂件,告诉Git项⽬中要忽略的⽂件或⽂件夹 
20 ├── README.md # 说明⽂档 
21 ├── babel.config.js # Babel 配置⽂件 
22 ├── package-lock.json # 记录安装时的包的版本号,以保证⾃⼰或其他⼈在 npm i nstall 时⼤家的依赖能保证⼀致 
23 ├── package.json # 包说明⽂件,记录了项⽬中使⽤到的第三⽅包依赖信息等内容 
24 └── tsconfig.json # TypeScript 配置⽂件

在Vue项目中启用TypeScript支持的两种方式:
(1)全新项目:使用Vue CLI脚手架工具创建Vue项目,勾选TypeScript选项
(2)已有项目:添加Vue官方配置的TypeScript适配插件
使用@vue/cli安装TypeScript插件
vue add @vue/typescript

4. TypeScript相关配置介绍

(1)安装了TypeScript相关的依赖项 dependencies依赖:

依赖项说明
vue-class-component提供使用Class语法写Vue组件
vue-property-decorator在Class语法基础上提供了一些辅助装饰器

devDependencies依赖:

依赖项说明
@typescript-eslint/eslint-plugin使用ESLint校验TypeScript代码
@typescript-eslint/parser将TypeScript转为 AST 共 ESLint 校验使用
@vue/cli-plugin-typescript使用TypeScript + ts-loader + fork-ts-checker-webpack-plugin进行更快的类型检查
@vue/eslint-config-typescript兼容ESLint的TypeScript 校验规则
typescriptTypeScript编译器,提供类型校验和转换javaScript功能

(2)TypeScript 配置文件 tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": [
      "webpack-env"
    ],
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

(3)shims-vue.d.ts文件的作用:
主要用于 TypeScript 识别 .vue 文件模块 TypeScript 默认不支持导入 .vue 模块,这个文件告诉 TypeScript 导入 .vue 文件模块都按照vueconstructor 类型识别处理

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

(4)shims-tsx.d.ts文件的作用:
为 jsx 组件模板补充类型声明,如果项目中没有使用到 jsx 可以忽略此文件

import Vue, { VNode } from 'vue'

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode {}
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

(5)TypeScript 模块都是用 .ts 后缀

5.定义组件的方式

(1)使用Options APIs 要让 TypeScript 正确推断Vue 组件选项中的类型,您需要使用 Vue.componentVue.extend定义组件:

<script lang="ts">
  import Vue from 'Vue'
  
  export default Vue.extend({
    // 以前怎么写,现在还怎么写
    data () {
      return {
        a: 1,
        b: '2',
        c: [],
        d: {
          e: 1,
          f: '2'
        }
      }
    },
    
    methos: {
      test () {
        this.a.abc() // 编辑器报错:Property 'abc' does not exist on type 'number'
      }
    }
  })
</script>

(2)使用Class APIs 在 TypeScript 下, Vue 的组件可以使用一个继承自Vue类型的子类表示, 这种类型需要使用 Component 装饰器取修饰 装饰器函数接收的参数就是以前的组件选项对象(data、props、methods之类)

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component' // 官方库

// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
  // 所有的组件选项都可以放在这里
  template: '<button @click="onClick">Click!</button>'
})
export default class MyComponent extends Vue {
  // 初始数据可以直接声明为实例的 property
  message: string = 'Hello!'

  // 组件方法也可以直接声明为实例的方法
  onClick (): void {
    window.alert(this.message)
  }
}
</script>

其他的如computed、props、mixin等参考vue-class-component文档,官方库文vue-class-component有些选项的写法比较麻烦,可以在项目中安装vue-property-decorator来简化一些选项的写法

装饰器语法可以参考阮一峰的es6-入门教程的相关内容。装饰器语法是ES草案中的一个新特性,可能会进行重大调整,所以并不建议在生产环境中使用。

6.代码格式规范

常用的标准:

// ESLint配置文件:.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  // 插件: 扩展了校验规则
  extends: [
    'plugin:vue/essential', // eslint-plugin-vue
    '@vue/standard',  // @vue/eslint-config-standard
    '@vue/typescript/recommended' // @vue/eslint-config-typescrip 
  ],
  parserOptions: {
    ecmaVersion: 2020
  },
  // 自定义验证规则
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}

{
    "rules": {
        "semi": ["error", "always"],
        "quotes": ["error", "double"]
    }
}

ESLint附带有大量的规则。你可以使用注释或者配置文件修改项目中要使用的规则。要改变一个规则设置,你必须讲规则 ID 设置为下列值之一:

  • "off"或者0——关闭规则
  • "warn"1——开启规则,使用警告级别的错误:"warn"(不会导致程序退出)
  • "error"2——开启规则,使用错误级别的错误:"error"(当被触发的时候,程序会退出) 在文件注释里配置规则,使用以下格式的注释:
/* eslint eqeqeq: "off", curly: "error" */

在这个例子里,eqeqeq规则被关闭,curly规则被打开,定义为错误级别。 如果一个规则有额外的选项,你可以使用数组字面量指定它们,比如:

 /* eslint quotes: ["error", "double"], curly: 2 */

这条注释为规则 quotes 指定了 “double”选项。数组的第⼀项总是规则的严重程度(数字或字符串)。
配置定义在插件中的⼀个规则的时候,你必须使⽤ 插件名/规则ID 的形式。⽐如:

{
    "plugins": [
        "plugin1"
    ],
    "rules": {
        "eqeqeq": "off",
        "curly": "error",
        "quotes": ["error", "double"],
        "plugin1/rule1": "error"
    }
}

在这些配置文件中,规则 plugin1/rule1 表示来自插件 plugin1rule1 规则。你也可以使用这种格式的注释配置,比如:

/* eslint "plugin1/rule1": "error" */

注意:当指定来自插件的规则时,确保删除 eslint-plugin- 前缀。ESLint 在内部只使用没有前缀的名称去定位规则。

7. 导入Element组件库

安装element:

npm i element-ui

main.js中导入配置

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
 Vue.use(ElementUI)

8. 处理样式

在src/styles文件夹下创建一下几个文件:

 // src/styles
 ├── index.scss # 全局样式(在⼊⼝模块被加载⽣效)
 ├── mixin.scss # 公共的 mixin 混⼊(可以把重复的样式封装为 mixin 混⼊到复⽤ 的地⽅)
 ├── reset.scss # 重置基础样式
 └── variables.scss # 公共样式变量

variables.scss

$primary-color: #40586F;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #868e96; // #22b8cf;

$body-bg: #E9EEF3; // #f5f5f9;

$sidebar-bg: #F8F9FB;
$navbar-bg: #F8F9FB;

$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

index.scss

@import './variables.scss';

// globals
html {
  font-family: $font-family;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  // better Font Rendering
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  background-color: $body-bg;
}

// custom element theme
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
// change font path, required
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// import element default theme
@import '~element-ui/packages/theme-chalk/src/index';
// node_modules/element-ui/packages/theme-chalk/src/common/var.scss

// overrides

// .el-menu-item, .el-submenu__title {
//   height: 50px;
//   line-height: 50px;
// }

.el-pagination {
  color: #868e96;
}

// components

.status {
  display: inline-block;
  cursor: pointer;
  width: .875rem;
  height: .875rem;
  vertical-align: middle;
  border-radius: 50%;

  &-primary {
    background: $--color-primary;
  }

  &-success {
    background: $--color-success;
  }

  &-warning {
    background: $--color-warning;
  }

  &-danger {
    background: $--color-danger;
  }

  &-info {
    background: $--color-info;
  }
}

共享全局样式变量: 参考:vue官方文档中CSS相关向预处理器 Loader 传递选项一节的介绍
项目根目录下创建vue.config.js文件,并进行如下配置

// vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      scss: {
        prependData: `@import "~@/variables.scss";`
      },
    }
  }
}

父组件改变子组件样式-深度选择器
建议你使用::v-deep的写法,它不仅css的>>>写法,而且它还是vue3.0 RFC中指定的写法。
而且原本/deep/的写法也本身就Chrome所废弃,你现在经常能在控制台中发现Chrome提示你不要使用/deep/的警告。

9. 接口处理-配置后端代理

接口跨域问题:最推荐的也是我⼯作中在使⽤的⽅式就是: cors 全称为 Cross Origin Resource Sharing(跨域资 源共享)。这种⽅案对于前端来说没有什么⼯作量,和正常发送请求写法上没有任何区别,⼯作量基本都 在后端这⾥。每⼀次请求,浏览器必须先以 OPTIONS 请求⽅式发送⼀个预请求(也不是所有请求都会发 送 options,展开介绍 点我),通过预检请求从⽽获知服务器端对跨源请求⽀持的 HTTP ⽅法。在确认 服务器允许该跨源请求的情况下,再以实际的 HTTP 请求⽅法发送那个真正的请求。推荐的原因是:只要 第⼀次配好了,之后不管有多少接⼝和项⽬复⽤就可以了,⼀劳永逸的解决了跨域问题,⽽且不管是开发 环境还是正式环境都能⽅便的使⽤。详细 MDN ⽂档。
但总有后端觉得麻烦不想这么搞,那纯前端也是有解决⽅案的。
在 dev 开发模式下可以下使⽤ webpack 的 proxy 使⽤也是很⽅便,参照 ⽂档 就会使⽤了,楼主⼀ 些个⼈项⽬使⽤的该⽅法。但这种⽅法在⽣产环境是不能使⽤的。在⽣产环境中需要使⽤ nginx 进⾏反 向代理。不管是 proxy 和 nginx 的原理都是⼀样的,通过搭建⼀个中转服务器来转发请求规避跨域的 问题。 | 开发环境 | 生产环境 | | :----: | :----: | | cors | cors | | proxy | nginx |

虽然其他的跨域方式都还有很多但都不推荐,真心主流的就这两种方式。
配置客户端层面的服务端代理跨域可以参考官方文档中的说明:cli.vuejs.org/zh/config/#…
下面是具体的操作流程: 在项目根目录下添加vue.config.js配置文件

module.exports = {
  ...
  devServer: {
    proxy: {
      '/front': {
        target: 'http://edufront.lagou.com',
        changeOrigin: true // 设置请求头中的 host 为 target,防⽌后端 反向代理服务器⽆法识别
      },
      '/boss': {
        target: 'http://eduboss.lagou.com',
        changeOrigin: true
      }
    }
  }
}

10. 封装请求模块

安装axios

npm i axios

创建src/utils/request.js

import axios from 'axios'

// 创建axios实例
const service = axios.creat({
  
})

// request 拦截器

// response 拦截器

export default service

二、布局

三、以 application/x-www-form-urlencoded 格式发送请求

axios 默认发送是 application/json格式的数据,转 application/x-www-form-urlencoded建议使用qs
安装:

npm i qs

使用:

import qs from 'qs';
const data = { 'bar': 123 };
const options = {
  method: 'POST',
  //headers: {
  // 'content-type': 'application/x-www-form-urlencoded'
  //cd},
  data: qs.stringify(data),
  url,
};
axios(options);

axios会根据data的类型自动设置请求headers中的Content-Type;如果 data 是普通对象,则 Content-type 是 application/json;如果 data 是 qs.stringify(data) 转换之后的数据:key=value&key=value, 则 Content-Type 会被设置为 application/x-www-form-urlencoded;如果 data 是 FormData 对象,则 Content-Type 是 multipart/form-data 更多内容参考

四、根据登录状态校验页面访问权限

登录成功后把用户相关信息存储到vuex中方便组件间的共享,为了避免刷新页面数据丢失需要把用户信息持久化到storage中;

// 提交登录表单信息
import { login } from '@/services/user'
import { Form } from 'element-ui'
async onSubmit () {
  try {
    // 1、表单验证
    await (this.$refs.form as Form).validate() // 注明Form类型,否则编辑器会提示错误

    // 2、验证通过 ->提交表单
    this.isLoginLoading = true
    const { data } = await login(this.form)
    // 3、处理请求结果
    //   失败-输出失败原因
    if (data.state !== 1) {
      this.$message.error(data.message)
    } else {
      // 1.登录成功,记录登录状态,状态需要能够全局访问(放到 Vuex 容器中)
      this.$store.commit('setUser', data.content)
      // 2.然后在访问需要登录的页面的时候判断有没有登录状态(路由拦截器)
      // 成功-跳转会将要访问的页面
      this.$router.push((this.$route.query.redirect as string) || '/')
      this.$message.success('登录成功')
    }
  } catch (err) {
    console.log('登录失败', err)
  }

  // 结束登录按钮的 loading
  this.isLoginLoading = false
}
// store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // 容器的状态实现了数据共享,在组件里面访问方便,但是没有持久化的功能
  state: {
    user: JSON.parse(window.localStorage.getItem('user') || 'null') // 当前登录用户状态
  },
  mutations: {
    // 修改热容器数据必须使用 mutation 函数
    setUser (state, payload) {
      state.user = JSON.parse(payload)

      // 为了防止页面刷新数据丢失,我们需要把 user 数据持久化
      window.localStorage.setItem('user', payload)
    }
  },
  actions: {
  },
  modules: {
  }
})

利用vue-router的全局前置守卫router.beforeEachmeta字段来处理访问权限

// router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import LayOut from '@/layout/index.vue'
import store from '@/store'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
    path: '/',
    component: LayOut,
    children: [
      {
        path: '', // 默认子路由
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue'),
        meta: {
          requiresAuth: true
        }
      },
      {
        path: '/role',
        name: 'role',
        component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
      },
      {
        path: '/menu',
        name: 'menu',
        component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
      },
      {
        path: '/resource',
        name: 'resource',
        component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
      },
      {
        path: '/course',
        name: 'course',
        component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
      },
      {
        path: '/user',
        name: 'user',
        component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
      },
      {
        path: '/advert',
        name: 'advert',
        component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
      },
      {
        path: '/advert-space',
        name: 'advert-space',
        component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
      }
    ]
  },
  {
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
  }
]

const router = new VueRouter({
  routes
})

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!store.state.user) {
      next({
        name: 'login',
        query: { // 通过 url 传递查询字符串参数
          redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面
        }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定调用 next()
  }
})

export default router

注意: (1)一个路由匹配到的所有路由记录会暴露为$route对象的matched数组,所以一旦父级路由限制登录访问的权限,则它下面的子路由也都需要登录后才可以访问,为了灵活一般都给子路由单独添加权限标识meta:{ requiresAuth: true};(2)确保next()方法一定要调用

五、使用请求拦截器统一设置Token

// store/index.js
import axios from 'axios'
import store from '@/store/index'

// 创建axios实例
const request = axios.create({

})

// request 拦截器
request.interceptors.request.use(function (config) {
  // 在这里通过改写 config 配置信息来实现业务功能统一处理
  const { user } = store.state
  if (user && user.access_token) {
    config.headers.Authorization = user.access_token
  }
  
  // 注意这里一定要返回 config ,否则请求就发送不出去了
  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})
// response 拦截器

export default request

六、关于Token过期问题

1. 概念介绍

access_token: 作用:获取需要授权的接口数据
expires_in: 作用:access_token 过期时间
refresh_token: 作用:刷新获取access_token

2. 处理Token过期的两种方法:

方法一:
在请求发起前拦截每个请求,判断 token 的有效时间是否已过期,若已过期,则将请求挂起,先刷新token后再继续请求。 优点:在请求前拦截,能节省请求,省流量
缺点:需要后端额外提供一个token过期时间的字段;使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。

方法二:
不在请求前拦截,而是拦截返回后的数据。先发起请求,接口返回过期后,先刷新token,再进行一次重试。
优点:不需要额外的token过期字段,不需要判断时间。
缺点:会消耗多一次请求,耗流量。

总结:方法一和方法二优缺点是互补的,方法一有校验失败的风险(本地时间被篡改时),方法二,等知道服务器已经过期了再重试一次,除了会多耗一个请求没有其他问题,推荐使用方法二。

3. 使用响应拦截器处理token过期

import axios from 'axios'
import store from '@/store/index'
import { Message } from 'element-ui' // 引入element-ui的消息提示组件
import router from '@/router' // 引入vue-router
import qs from 'qs'

// 创建axios实例
const request = axios.create({

})

function redirectLogin () {
  router.push({
    name: 'login',
    query: {
      redirect: router.currentRoute.fullPath // 记录当前页面地址,登录成功重新返回回来
    }
  })
}

function refreshToken () {
  return axios.create()({
    method: 'POST',
    url: '/front/user/refresh_token',
    data: qs.stringify({
      refreshtoken: store.state.user.refresh_token
    })
  })
}

// 省略request 拦截器(参见上文五)

// response 拦截器
let isRefreshing = false // 控制刷新 token 的状态
let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求
request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里
  // 如果是自定义错误状态码,错误处理就写这里
  return response
}, async function (error) { // 超出 2xx 状态码都执行这里
  // 如果是使用的 HTTP 状态码,错误处理就写这里
  if (error.response) { // 请求发出去收到响应了,但是状态码超出了 2xx 范围
    // 状态码根据与服务端协商处理
    const { status } = error.response
    if (status === 400) {
      Message.error('请求参数错误')
    } else if (status === 401) {
      // token 无效(没有提供 token、是无效的、token过期了)
      // 如果没有,则直接跳转登录页
      if (!store.state.user) {
        redirectLogin()
        return Promise.reject(error)
      }

      // 如果有 refresh_token 则尝试使用 refresh_token 获取新的access_token
      if (!isRefreshing) {
        isRefreshing = true // 开启刷新状态
        // 尝试刷新获取新的 token
        return refreshToken().then(res => {
          if (!res.data.success) {
            throw new Error('刷新 Token 失败')
          }
          // 刷新 token 成功
          store.commit('setUser', res.data.content) // 把刷新拿到的新的 access_token 更新到容器和本地存储中
          // 把 requests 队列中的请求重新发出去
          requests.forEach(cb => cb())
          // 队列中的请求发送后,需要重置requests 数组
          requests = []
          // 把本次失败的请求重新发出去
          return request(error.config) // error.config中包含失败请求的方法、url、参数
        }).catch(err => {
          console.log(err)
          // 把当前登录用户的状态清除
          store.commit('setUser', null)
          // 刷新 token 失败 ——> 跳转登录页面重新登录获取新的 token
          redirectLogin()
          return Promise.reject(error)
        }).finally(() => {
          isRefreshing = false // 无论刷新 token 成功还是失败,最后都需要重置刷新状态
        })
      }

      // 刷新状态下,把请求挂起放到 requests 数组中
      return new Promise(resolve => {
        requests.push(() => {
          resolve(request(error.config))
        })
      })
    } else if (status === 403) {
      Message.error('没有权限,请联系管理员')
    } else if (status === 404) {
      Message.error('请求资源不存在')
    } else if (status >= 500) {
      Message.error('服务端错误,请联系管理员')
    }
  } else if (error.request) { // 请求发出去没有收到响应
    Message.error('请求超时,请刷新重试')
  } else { // 在设置请求时发生了一些事情,触发了一个错误
    Message.error(`请求失败:${error.message}`)
  }
  // 把请求失败的错误对象继续抛出,扔给上一个调用者
  return Promise.reject(error)
})

export default request

七、axios错误处理

import axios from 'axios'
import store from '@/store/index'
import { Message } from 'element-ui' // 引入element-ui消息提示组件

// 创建axios实例
const request = axios.create({

})

// request 拦截器(省略,见上文五)

// response 拦截器
request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里
  // 如果是自定义错误状态码,错误处理就写这里
  return response
}, function (error) { // 超出 2xx 状态码都执行这里
  // 如果是使用的 HTTP 状态码,错误处理就写这里
  if (error.response) { // 请求发出去收到响应了,但是状态码超出了 2xx 范围
    // 状态码根据与服务端协商处理
    const { status } = error.response
    if (status === 400) {
      Message.error('请求参数错误')
    } else if (status === 401) {
      // token 无效(没有提供 token、是无效的、token过期了)
    } else if (status === 403) {
      Message.error('没有权限,请联系管理员')
    } else if (status === 404) {
      Message.error('请求资源不存在')
    } else if (status >= 500) {
      Message.error('服务端错误,请联系管理员')
    }
  } else if (error.request) { // 请求发出去没有收到响应
    Message.error('请求超时,请刷新重试')
  } else { // 在设置请求时发生了一些事情,触发了一个错误
    Message.error(`请求失败:${error.message}`)
  }
  // 把请求失败的错误对象继续抛出,扔给上一个调用者
  return Promise.reject(error)
})

export default request