Vue3+TS:axios封装

本文整理来自深入Vue3+TypeScript技术栈-coderwhy大神新课,只作为个人笔记记录使用,请大家多支持王红元老师。

认识axios

为什么选择axios? 因为作者推荐。

功能特点:

  • 在浏览器中发送 XMLHttpRequests 请求
  • 在 node.js 中发送 http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 等等

补充:axios名称的由来?
个人理解,没有具体的翻译,axios:ajax i/o system.

axios请求方式

支持多种请求方式:

  • axios(config)
  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])
import axios from 'axios'

// axios的实例对象
// get请求
axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {
  console.log(res.data)
})

// 额外补充的Promise中类型的使用
// Promise本身是可以有类型
new Promise<string>((resolve) => {
  // 泛型指定了,只能传string
  resolve('abc')
}).then((res) => {
  // 并且res也是string类型的
  console.log(res.length)
}

// 使用http://httpbin.org模拟数据请求

// get请求,并且传入参数
axios
  .get('http://httpbin.org/get', {
    // get请求使用params传参,并且最后会拼接到url后面
    params: {
      name: 'coderwhy',
      age: 18
    }
  })
  .then((res) => {
    console.log(res.data)
  })

// post请求,传入参数
axios
  .post('http://httpbin.org/post', {
    // post请求使用data传参
    data: {
      name: 'why',
      age: 18
    }
  })
  .then((res) => {
    console.log(res.data)
  })

有时候,我们可能需求同时发送两个请求,使用axios.all, 可以放入多个请求的数组,当所有请求完成之后,axios.all([]) 会返回一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2。

// axios.all -> 多个请求, 一起返回
axios
  .all([
    axios.get('/get', { params: { name: 'why', age: 18 } }),
    axios.post('/post', { data: { name: 'why', age: 18 } })
  ])
  .then((res) => {
    // 结果是个数组
    console.log(res[0].data)
    console.log(res[1].data)
  })

axios常见的配置选项

有时候我们需要配置请求的baseURL和timeout,具体其他配置如下:

解释 配置选项
请求地址 url: '/user'
请求类型 method: 'get'
请求根路径 baseURL: 'http://www.mt.com/api'
请求前的数据处理 transformRequest:[function(data){}]
请求后的数据处理 transformResponse: [function(data){}]
自定义的请求头 headers:{'x-Requested-With':'XMLHttpRequest'}
URL查询对象 params:{ id: 12 }
查询对象序列化函数 paramsSerializer: function(params){ }
request body data: { key: 'aa'}
超时设置 timeout: 1000
跨域是否带Token withCredentials: false
自定义请求处理 adapter: function(resolve, reject, config){}
身份验证信息 auth: { uname: '', pwd: '12'}
响应的数据格式(json、blob、document、arraybuffer、text、stream) responseType: 'json'
// axios的配置选项
// 全局的配置 baseURL timeout headers
axios.defaults.baseURL = 'http://httpbin.org'
axios.defaults.timeout = 10000
// axios.defaults.headers = {}

// 每一个请求单独的配置 timeout headers
axios
  .get('/get', {
    params: {
      name: 'coderwhy',
      age: 18
    },
    // 单独配置
    timeout: 5000,
    headers: {}
  })
  .then((res) => {
    console.log(res.data)
  })

// post请求
axios
  // 全局配置baseURL之后就不用再写url了
  .post('/post', {
    data: {
      name: 'why',
      age: 18
    }
  })
  .then((res) => {
    console.log(res.data)
  })

axios的实例和拦截器

为什么要创建axios的实例呢?
当我们从axios模块中导入对象时,使用的实例是默认的实例,当给该实例设置一些默认配置时,这些配置就被固定下来了。但是后续开发中,某些配置可能会不太一样,比如某些请求需要使用特定的baseURL或者timeout或者content-Type等,这个时候, 我们就可以创建新的实例,并且传入属于该实例的配置信息。

axios也可以设置拦截器,拦截每次请求和响应:

  • axios.interceptors.request.use(请求成功拦截, 请求失败拦截)
  • axios.interceptors.response.use(响应成功拦截, 响应失败拦截)
// axios的拦截器
// 参数fn1: 请求发送成功会执行的函数
// 参数fn2: 请求发送失败会执行的函数
axios.interceptors.request.use(
  (config) => {
    // 想做的一些操作
    // 1.给请求添加token
    // 2.添加isLoading动画
    console.log('请求成功的拦截')
    return config
  },
  (err) => {
    console.log('请求发送错误')
    return err
  }
)

// fn1: 数据响应成功(服务器正常的返回了数据 200)
// fn2: 数据响应失败
axios.interceptors.response.use(
  (res) => {
    console.log('响应成功的拦截')
    return res
  },
  (err) => {
    console.log('服务器响应失败')
    return err
  }
)

区分不同环境

在开发中,有时候我们需要根据不同的环境设置不同的环境变量,常见的有三种环境:

  • 开发环境:development;
  • 生产环境:production;
  • 测试环境:test;

如何区分环境变量呢?常见有三种方式:
方式一:手动修改不同的变量(不推荐)。
方式二:根据process.env.NODE_ENV的值进行区分,这种方式也是使用很多的一种方式(推荐)。

// 根据process.env.NODE_ENV区分
// 开发环境: development
// 生成环境: production
// 测试环境: test

let BASE_URL = ''
const TIME_OUT = 10000

if (process.env.NODE_ENV === 'development') {
  BASE_URL = 'http://123.207.32.32:8000/'
} else if (process.env.NODE_ENV === 'production') {
  BASE_URL = 'http://coderwhy.org/prod'
} else {
  BASE_URL = 'http://coderwhy.org/test'
}

export { BASE_URL, TIME_OUT }

方式三:编写不同的环境变量配置文件,vue cli支持这种方式,我们创建.env.development.env.production.env.test文件。

通过环境变量配置文件,我们可以给BASE_URLNODE_DEV设置值,这些值会自动被注入,如果是我们自定义的名字,可以VUE_APP_XXX这种格式的也可以被注入。

.env.development文件:

VUE_APP_BASE_URL=https://coderwhy.org/dev
VUE_APP_BASE_NAME=coderwhy

.env.production文件:

VUE_APP_BASE_URL=https://coderwhy.org/prod
VUE_APP_BASE_NAME=kobe

.env.test文件:

VUE_APP_BASE_URL=https://coderwhy.org/test
VUE_APP_BASE_NAME=james

在JS中,我们直接使用不会报错,在TS中直接使用会报错:

// 报错
console.log(process.env.VUE_APP_BASE_URL)

我们在shims-vue.d.ts文件中声明一下即可:

declare const VUE_APP_BASE_URL: string

npm run build打包项目,打开打包后的build文件夹下的index.html文件,通过live serve打开index.html文件,这时候很多文件是加载不到的:

加载不到的原因是因为上面的路径是根据域名拼接的绝对路径,我们可以进入index.html中,将加载文件的路径改成相对路径:src="./js/chunk-xxxxxx.js",也就是加载当前路径下的js文件夹下的文件,一个一个改路径,比较麻烦。

打包之后,如果不想手动一个一个改路径,可以进入vue.config.js文件中,添加publicPath: './',这个值其实就是修改加载资源的路径,但是部署到服务器的时候肯定不需要这个值了,注释掉即可,或者加个环境判断也可以。

封装axios

先讲一下逻辑,我们将其封装成一个对象(为什么封装成类呢?因为使用类封装性更强一点),新建service文件夹,service文件夹里面再新建request文件夹和index.ts文件,然后在main.ts里面引入这个对象,然后就可以使用了,文件目录如下:

首先要安装axios:

npm install axios

先说一下我们封装要达到的目的:可以对某个请求、某个请求实例的所有请求、所有请求实例的所有请求,设置拦截和是否显示loading。

下面就对每个文件的代码以及作用进行讲解:

config.ts代码如下,会根据环境配置不同的BASE_URL。

// 根据process.env.NODE_ENV区分
// 开发环境: development
// 生成环境: production
// 测试环境: test

let BASE_URL = ''
const TIME_OUT = 10000

if (process.env.NODE_ENV === 'development') {
  BASE_URL = 'http://123.207.32.32:8000/'
} else if (process.env.NODE_ENV === 'production') {
  BASE_URL = 'http://coderwhy.org/prod'
} else {
  BASE_URL = 'http://coderwhy.org/test'
}

export { BASE_URL, TIME_OUT }

在type.js里面我们定义一个接口,用于规定创建请求实例或者调用request方法的时候传入的参数是什么样的,代码如下:

import type { AxiosRequestConfig, AxiosResponse } from 'axios'

// 定义一个接口,表示这个接口的实例要有这4个属性,当然不是必须的,是可选的
// 传入一个泛型,默认值是AxiosResponse
export interface HYRequestInterceptors<T = AxiosResponse> {
  // 拦截器都是可选的
  // 请求拦截
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  // 请求错误拦截
  requestInterceptorCatch?: (error: any) => any
  // 响应拦截
  // 由于我们在前面直接将res.data返回了,所以这里如果传入了T,那么返回的类型就是传入的T
  responseInterceptor?: (res: T) => T
  // 响应错误拦截
  responseInterceptorCatch?: (error: any) => any
}

// 定义一个新的接口,继承于AxiosRequestConfig,表示我们传入的参数要有interceptors和showLoading,当然也是可选的
export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  // 对原来的AxiosRequestConfig进行扩展,添加拦截器和是否显示loading,可选的
  interceptors?: HYRequestInterceptors<T>
  showLoading?: boolean
}

核心代码就是request文件夹下的index.js文件,代码如下:

import axios from 'axios'
// 导入axios实例的类型
import type { AxiosInstance } from 'axios'
import type { HYRequestInterceptors, HYRequestConfig } from './type'

// 引入loading组件
import { ElLoading } from 'element-plus'
// 引入loading组件的类型
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'

// 默认显示loading
const DEAFULT_LOADING = true

class HYRequest {
  // axios实例
  instance: AxiosInstance
  // 当前请求实例的拦截器
  interceptors?: HYRequestInterceptors
  // 是否显示loading
  showLoading: boolean
  // 保存的loading实例
  loading?: ILoadingInstance

  constructor(config: HYRequestConfig) {
    // 创建axios实例
    this.instance = axios.create(config)
    // 保存基本信息
    this.interceptors = config.interceptors
    this.showLoading = config.showLoading ?? DEAFULT_LOADING

    // 使用拦截器
    // 1.从config中取出的拦截器是对应的实例的拦截器
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )

    // 2.添加所有的实例都有的拦截器
    // 请求的时候,先添加的拦截器后执行
    // 响应的时候,先添加的拦截器先执行
    this.instance.interceptors.request.use(
      (config) => {
        console.log('所有的实例都有的拦截器: 请求成功拦截')

        // 所有的请求都添加loading
        if (this.showLoading) {
          // 添加loading
          this.loading = ElLoading.service({
            lock: true,
            text: '正在请求数据....',
            background: 'rgba(0, 0, 0, 0.5)'
          })
        }
        return config
      },
      (err) => {
        console.log('所有的实例都有的拦截器: 请求失败拦截')
        return err
      }
    )

    this.instance.interceptors.response.use(
      (res) => {
        console.log('所有的实例都有的拦截器: 响应成功拦截')
        // 所有的请求,将loading移除
        this.loading?.close()

        // 因为我们需要的就是res.data,所以我们可以在所有请求实例的请求的响应拦截器里面,直接把res.data返回,这样我们就可以直接使用了
        const data = res.data
        // 判断当HttpErrorCode是200的时候,服务端和客户端一块自定义的错误信息
        if (data.returnCode === '-1001') {
          console.log('请求失败~, 错误信息')
        } else {
          return data
        }
      },
      (err) => {
        console.log('所有的实例都有的拦截器: 响应失败拦截')
        // 所有的请求,将loading移除
        this.loading?.close()

        // 判断不同的HttpErrorCode显示不同的错误信息
        if (err.response.status === 404) {
          console.log('404的错误~')
        }
        return err
      }
    )
  }

  // 1.传入返回结果的类型T,这样在Promise中我们就知道返回值的类型是T了
  // 2.通过HYRequestConfig<T>,将返回值类型T告诉接口,从而在接口的返回响应拦截中指明返回值类型就是T
  request<T>(config: HYRequestConfig<T>): Promise<T> {
    // 返回一个Promise对象,好让使用者在外面拿到数据
    return new Promise((resolve, reject) => {
      // 1.单个请求对请求config的处理
      if (config.interceptors?.requestInterceptor) {
        // 如果有单个请求的拦截器,就执行一下这个函数,然后返回
        config = config.interceptors.requestInterceptor(config)
      }

      // 2.判断单个请求是否需要显示loading
      if (config.showLoading === false) {
        this.showLoading = config.showLoading
      }

      this.instance
        // request里面有两个泛型,第一个泛型默认是any,第二个泛型是AxiosResponse
        // 由于前面我们已经将res.data直接返回了,所以其实最后的数据就是T类型的,所以我们在第二个泛型中要指定返回值的类型T
        .request<any, T>(config)
        .then((res) => {
          // 1.单个请求对数据的处理
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }
          // 2.将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEAFULT_LOADING

          // 3.将结果resolve返回出去
          resolve(res)
        })
        .catch((err) => {
          // 将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEAFULT_LOADING
          reject(err)
          return err
        })
    })
  }

  get<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  delete<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  patch<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}

export default HYRequest

这时候我们需要创建一个请求实例,用于发送网络请求,当然我们也可以创建不止一个请求实例,然后设置不同的baseurl、超时时间、拦截器等等,这里我们只创建一个,所以外层的index.ts代码如下:

// service统一出口
import HYRequest from './request'
import { BASE_URL, TIME_OUT } from './request/config'

// 创建一个新的请求,并传入参数
const hyRequest = new HYRequest({
  // 传入baseurl
  baseURL: BASE_URL,
  // 传入超时时间
  timeout: TIME_OUT,
  // 传入拦截器
  interceptors: {
    requestInterceptor: (config) => {
      // 给当前请求实例所有的请求添加token
      const token = ''
      if (token) {
        // 模板字符串进行拼接
        config.headers.Authorization = `Bearer ${token}`
      }

      console.log('请求成功的拦截')
      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('请求失败的拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('响应成功的拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('响应失败的拦截')
      return err
    }
  }
})

export default hyRequest

在main.ts中使用如下:

import { createApp } from 'vue'
import App from './App.vue'
// 导入请求实例
import hyRequest from './service'

const app = createApp(App)
app.mount('#app')

hyRequest.request({
  url: '/home/multidata',
  method: 'GET',
  headers: {},
  interceptors: {
    requestInterceptor: (config) => {
      console.log('单独请求的config')
      config.headers['token'] = '123'
      return config
    },
    responseInterceptor: (res) => {
      console.log('单独响应的response')
      return res
    }
  }
})

// 定义返回结果的类型
interface DataType {
  data: any
  returnCode: string
  success: boolean
}

// 只有请求者才知道返回结果的类型
hyRequest
  .get<DataType>({
    url: '/home/multidata',
    showLoading: false
  })
  // 这时候这里的res就是DataType类型的
  .then((res) => {
    console.log(res.data)
    console.log(res.returnCode)
    console.log(res.success)
  })

注意:Vuex和TS的结合比较难用,所以如果使用TS,我们一般使用pinia这个库来代替Vuex。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容