导读

先捋一捋前后端交互请求的API方式都有哪些吧!

首先明确我们现在学习和使用的是Vue3.js,那么选用VueRequest库自然是比较不错的选择,当然了Fetch和Axios还是会在合适的地方使用的,即便是繁杂的原始xhr也会在某些高级功能的时候需要我们使用

大体如下:

  • Fetch
    Fetch是浏览器提供的用于进行网络请求的API,使用起来相对简单。它返回一个Promise对象,支持请求和响应的headers、body、response type等属性的设置。
    Fetch的优点在于它是浏览器自带的API,不需要安装额外的库或插件,而且可以轻松地发送和接收JSON数据。

  • XMLHttpRequest (XHR)
    XMLHttpRequest (XHR)是一个早期的用于进行异步HTTP请求的API。它可以从服务器获取数据,也可以将数据发送到服务器。XHR的优点在于它的支持性较好,可以在所有主流的浏览器中使用。
    XHR的缺点在于,它的API较为繁琐,需要手动设置请求头、发送数据等。而且在处理复杂的请求时,代码会变得复杂且难以维护。

  • Axios
    Axios是一个流行的用于进行HTTP请求的JavaScript库。它可以在浏览器和Node.js中使用,支持Promise API、请求和响应拦截器、数据转换、自动CSRF等功能。Axios还提供了一些方便的API,例如axios.get、axios.post等。
    Axios的优点在于它的API简单易用,而且提供了许多有用的功能,例如请求拦截器和响应拦截器,可以方便地在请求或响应被发送或接收时进行一些处理。此外,Axios还支持取消请求,可以在请求已经被发送但是服务器还未响应时取消该请求。

  • vue-request
    vue-request是一个基于Vue.js的第三方组件库,提供了一些常用的HTTP请求和响应处理功能。它封装了XMLHttpRequest和fetch,支持自定义的拦截器和自动重试。
    vue-request的优点在于它是基于Vue.js的,可以方便地集成到Vue.js项目中。此外,vue-request提供了一些额外的功能,例如自动重试和拦截器,可以提高代码的可重用性和可维护性。

xhr

浏览器中有两套 API 可以和后端交互,发送请求、接收响应,fetch api 前面我们已经介绍过了,另一套 api 是 xhr,基本用法如下

const xhr = new XMLHttpRequest()
xhr.onload = function() {
    console.log(xhr.response)
}
xhr.open('GET', 'http://localhost:8080/api/students')
xhr.responseType = "json"
xhr.send()

但这套 api 虽然功能强大,但比较老,不直接支持 Promise,因此有必要对其进行改造

function get(url: string) {
  return new Promise((resolve, reject)=>{
    const xhr = new XMLHttpRequest()
    xhr.onload = function() {
      if(xhr.status === 200){
        resolve(xhr.response)
      } else if(xhr.status === 404) {
        reject(xhr.response)
      } // 其它情况也需考虑,这里简化处理
    }
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.send()
  })
}
  • Promise 对象适合用来封装异步操作,并可以配合 await 一齐使用
  • Promise 在构造时,需要一个箭头函数,箭头函数有两个参数 resolve 和 reject
    • resolve 是异步操作成功时被调用,把成功的结果传递给它,最后会作为 await 的结果返回
    • reject 在异步操作失败时被调用,把失败的结果传递给它,最后在 catch 块被捉住
  • await 会一直等到 Promise 内调用了 resolve 或 reject 才会继续向下运行

调用示例1:同步接收结果,不走代理

try {
  const resp = await get("http://localhost:8080/api/students")
  console.log(resp)
} catch (e) {
  console.error(e)
}

调用示例2:走代理

try {
  const resp = await get('/api/students')
  console.log(resp)  
} catch(e) {
  console.log(e)
}

走代理明显慢不少

axios

基本用法

axios 就是对 xhr api 的封装,手法与前面例子类似

安装

npm install axios

一个简单的例子

<script setup lang="ts">
import { ref, onMounted } from "vue";
import axios from "axios";

let count = ref(0);

async function getStudents() {
  try {
    const resp = await axios.get("/api/students");
    count.value = resp.data.data.length;
  } catch (e) {
    console.log(e);
  }
}

onMounted(() => {
  getStudents()
})
</script>

<template>
  <h2>学生人数为:{{ count }}</h2>
</template>
  • onMounted 指 vue 组件生成的 html 代码片段,挂载完毕后被执行

再来看一个 post 例子

<script setup lang="ts">
import { ref } from "vue";
import axios from "axios";

const student = ref({
  name: '',
  sex: '男',
  age: 18
})

async function addStudent() {
  console.log(student.value)
  const resp = await axios.post('/api/students', student.value)
  console.log(resp.data.data)
}
</script>

<template>
  <div>
    <div>
      <input type="text" placeholder="请输入姓名" v-model="student.name"/>
    </div>
    <div>
      <label for="">请选择性别</label>
      男 <input type="radio" value="男" v-model="student.sex"/> 
      女 <input type="radio" value="女" v-model="student.sex"/>
    </div>
    <div>
      <input type="number" placeholder="请输入年龄" v-model="student.age"/>
    </div>
    <div>
      <input type="button" value="添加" @click="addStudent"/>
    </div>
  </div>
</template>
<style scoped>
div {
  font-size: 14px;
}
</style>

环境变量

  • 开发环境下,联调的后端服务器地址是 http://localhost:8080
  • 上线改为生产环境后,后端服务器地址为 http://itheima.com

这就要求我们区分开发环境和生产环境,这件事交给构建工具 vite 来做
默认情况下,vite 支持上面两种环境,分别对应根目录下两个配置文件

  • .env.development - 开发环境
  • .env.production - 生产环境

针对以上需求,分别在两个文件中加入

VITE_BACKEND_API_BASE_URL = 'http://localhost:8080'

VITE_BACKEND_API_BASE_URL = 'http://itheima.com'

然后在代码中使用 vite 给我们提供的特殊对象 import.meta.env,就可以获取到 VITE_BACKEND_API_BASE_URL 在不同环境下的值

import.meta.env.VITE_BACKEND_API_BASE_URL

默认情况下,不能智能提示自定义的环境变量,做如下配置:新增文件 src/env.d.ts 并添加如下内容

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_BACKEND_API_BASE_URL: string
  // 更多环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

baseURL

可以自己创建一个 axios 对象,方便添加默认设置,新建文件 /src/api/request.ts

// 创建新的 axios 对象
import axios from 'axios'
const _axios = axios.create({
  baseURL: import.meta.env.VITE_BACKEND_API_BASE_URL
})

export default _axios

然后在其它组件中引用这个 ts 文件,例如 /src/views/E8.vue,就不用自己拼接路径前缀了

<script setup lang="ts">
import axios from '../api/request'
// ...
await axios.post('/api/students', ...)    
</script>

拦截器

// 创建新的 axios 对象
import axios from 'axios'
const _axios = axios.create({
  baseURL: import.meta.env.VITE_BACKEND_API_BASE_URL
})

// 请求拦截器
_axios.interceptors.request.use(
  (config)=>{ // 统一添加请求头
    config.headers = {
      Authorization: 'aaa.bbb.ccc'
    }
    return config
  },
  (error)=>{ // 请求出错时的处理
    return Promise.reject(error)
  }
)

// 响应拦截器
_axios.interceptors.response.use(
  (response)=>{ // 状态码  2xx
    // 这里的code是自定义的错误码
    if(response.data.code === 200) {
      return response
    }     
    else if(response.data.code === 401) {       
      // 情况1
      return Promise.resolve({})
    }
    // ... 
  },
  (error)=>{ // 状态码 > 2xx, 400,401,403,404,500
    console.error(error) // 处理了异常
    if(error.response.status === 400) {
      // 情况1
    } else if(error.response.status === 401) {
      // 情况2
    } 
    // ...
    return Promise.resolve({})
  }
)

export default _axios

处理响应时,又分成两种情况

  1. 后端返回的是标准响应状态码,这时会走响应拦截器第二个箭头函数,用 error.response.status 做分支判断
  2. 后端返回的响应状态码总是200,用自定义错误码表示出错,这时会走响应拦截器第一个箭头函数,用 response.data.code 做分支判断

另外

  • Promise.reject(error) 类似于将异常继续向上抛出,异常由调用者(Vue组件)来配合 try ... catch 来处理
  • Promise.resolve({}) 表示错误已解决,返回一个空对象,调用者中接到这个空对象时,需要配合 ?. 来避免访问不存在的属性

vue-request请求库

响应式的 axios 封装,官网地址 一个 Vue 请求库 | VueRequest (attojs.org)

首先安装 vue-request

npm install vue-request@next

useRequest

组件

<template>
  <h3 v-if="students.length === 0">暂无数据</h3>
  <ul v-else>
    <li v-for="s of students" :key="s.id">
      <span>{{s.name}}</span>
      <span>{{s.sex}}</span>
      <span>{{s.age}}</span>
    </li>
  </ul>
</template>
<script setup lang="ts">
import axios from "../api/request"
import { useRequest } from 'vue-request'
import { computed } from 'vue'
import { AxiosRespList, Student } from '../model/Model8080'

// data 代表就是 axios 的响应对象
const { data } = useRequest<AxiosRespList<Student>>(() => axios.get('/api/students'))

const students = computed(()=>{
  return data?.value?.data.data || []
})
</script>
<style scoped>
ul li {
  list-style: none;
  font-family: "华文行楷";
}

li span:nth-child(1) {
  font-size: 24px;
}
li span:nth-child(2) {
  font-size: 12px;
  color: crimson;
  vertical-align: bottom;
}
li span:nth-child(3) {
  font-size: 12px;
  color: darkblue;
  vertical-align: top;
}
</style>

data.value 的取值一开始是 undefined,随着响应返回变成 axios 的响应对象
用 computed 进行适配

usePagination

在 src/model/Model8080.ts 中补充类型说明

export interface StudentQueryDto {
  name?: string,
  sex?: string,
  age?: string, // 18,20
  page: number,
  size: number
}

js 中类似于 18,20 这样以逗号分隔字符串,会在 get 传参时转换为 java 中的整数数组

编写组件

<template>
  <input type="text" placeholder="请输入姓名" v-model="dto.name">
  <select v-model="dto.sex">
    <option value="" selected>请选择性别</option>
    <option value="男">男</option>
    <option value="女">女</option>
  </select>
  <input type="text" placeholder="请输入年龄范围" v-model="dto.age">
  <br>
  <input type="text" placeholder="请输入页码" v-model="dto.page">
  <input type="text" placeholder="请输入页大小" v-model="dto.size">
  <input type="button" value="搜索" @click="search">
  <hr>
  <h3 v-if="students.length === 0">暂无数据</h3>
  <ul v-else>
    <li v-for="s of students" :key="s.id">
      <span>{{s.name}}</span>
      <span>{{s.sex}}</span>
      <span>{{s.age}}</span>
    </li>
  </ul>
  <hr>
  总记录数{{total}} 总页数{{totalPage}}
</template>
<script setup lang="ts">
import axios from "../api/request"
import { usePagination } from 'vue-request'
import { computed, ref } from 'vue'
import { AxiosRespPage, Student, StudentQueryDto } from '../model/Model8080'

const dto = ref<StudentQueryDto>({name:'', sex:'', age:'', page:1, size:5})

// data 代表就是 axios 的响应对象
// 泛型参数1: 响应类型
// 泛型参数2: 请求类型
const { data, total, totalPage, run } = usePagination<AxiosRespPage<Student>, StudentQueryDto[]>(
  (d) => axios.get('/api/students/q', {params: d}), // 箭头函数
  {
    defaultParams: [ dto.value ], // 默认参数, 会作为参数传递给上面的箭头函数
    pagination: {
      currentKey: 'page', // 指明当前页属性
      pageSizeKey: 'size', // 指明页大小属性
      totalKey: 'data.data.total' // 指明总记录数属性
    } 
  } // 选项
)

const students = computed(()=>{
  return data?.value?.data.data.list || []
})

function search() {
  run(dto.value) // 会作为参数传递给usePagination的箭头函数
}
</script>
<style scoped>
ul li {
  list-style: none;
  font-family: "华文行楷";
}

li span:nth-child(1) {
  font-size: 24px;
}
li span:nth-child(2) {
  font-size: 12px;
  color: crimson;
  vertical-align: bottom;
}
li span:nth-child(3) {
  font-size: 12px;
  color: darkblue;
  vertical-align: top;
}
input,select {
  width: 100px;
}
</style>

usePagination 只需要定义一次,后续还想用它内部的 axios 发请求,只需调用 run 函数