1、数据请求的概念

如果现在有一个用户,他使用相应的计算机、平板电脑或者智能手机等终端设备,打开里面的浏览器尝试访问一个网站地址,那么在这个行为背后会发生哪些事情呢?

浏览器首先会进行域名解析。所谓域名解析,就像快递员送快递的行为一样,快递员知道一个收件人的家庭地址,但此时快递员并不能将快递准确、快速地送达。这是因为他需要先根据这个地址找到对应的区名,再根据信息去配送。而这个行为在网络上对应的就是要找到这个网站所在的服务器地址,每个网站对应的地址都是一个IP地址,就像58.218.215.132这样的唯一地址一样。

网站地址打开的过程,就像快递员与收件人之间的关系一样,快递员会先打电话给收件人以便确认送货,这个过程也就是请求的过程。

此时用户已经发送了request请求信息,请求信息到达服务器端后,服务器端就可以利用各种各样的服务器端程序语言(如Node、Java、PHP等)做相应的工作。通过它们和数据库衔接,从而操作数据。除此之外,服务器端还可以对文件进行创建、修改、删除等操作,最终返回内容,内容通常为HTML、TEXT、JSON等类型的数据,这就是所谓的response响应。

在request请求和response返回响应的过程中都会附带一些信息元内容。它们通过headers头信息进行描述和传递,因此headers头信息的概念也显得尤为重要,此概念会在后续的学习中重点提及。

现在我们已经简单描述了request请求和response响应传递操作。简单来说,它们是通过一定标准的协议来进行约束的,如果缺少任意一个协议,双方就无法达成共识,请求与返回响应也就无法正常地实现。

在这里插入图片描述
在数据请求过程中会涉及很多协议,我们了解最多的就是HTTP协议(Hyper Text Transfer Protocol)和HTTPS协议(Hypertext Transfer Protocol secure)。

HTTP协议是一种标准的超文本传输协议,它简单地定义了如何有效地将数据从客户端传输到服务器端。

HTTPS协议与HTTP协议不同。HTTPS是一种更为安全的协议,将传输的数据进行加密。因此,如果有人欺骗了你的请求链接,那么他们也无法读取你的数据。

其实不管是HTTP还是HTTPS协议,它们都只是URL结构的一小部分。在5.6.1节中已经强调过URL结构的组成部分,主要包括协议名(protocol)、域名(hostname)、端口号(port)、路径(path)、查询参数(query parameters)和锚点(anchor)。我们的目标是在Vue项目中进行URL的请求,通过域名解析等环节将request请求发送到服务器端,在服务器端进行处理后,客户端将服务器端response响应返回的数据内容接收,再进行当前项目的后续操作处理。

2、数据接口

2.1、什么是接口

想要知道什么是接口,我们需要先明确一个专业术语——API(Application Programming Interface,应用程序编程接口)​,它是一些预先定义的函数,目的是提供应用程序与开发人员访问一组例程的能力,而其又无须访问源码,或理解内部工作机制的细节。也就是说,开发人员可以使用API进行编程开发,而又无须访问源码或理解内部工作机制的细节。

因为前后端之间更多的是要实现数据之间的交互,所以在前后端之间,甚至外部系统与内部系统之间,或者内部系统的各个子系统之间,就产生了类似于API形式的数据接口交互使用。也就是说,数据接口就是将各个系统或层次之间的交互点,通过一些特殊的规则(或者将规则称为协议)进行数据之间的交互,这也是数据接口的应用需求。

2.2、接口的类型有哪些

1. Web Service接口

Web Service也称为Web服务,是一种跨程序语言和操作系统平台的远程调用技术。Web Service接口采用标准的SOAP(Simple Object Access Protocol,简单对象访问协议)传输。SOAP协议属于W3C标准,是基于HTTP的应用层协议传输XML数据的。

Web Service接口采用WSDL(Web ServicesDescription Language,万维网服务描述语言,用于描述Web服务发布的XML格式)作为描述语言。也就是说,WSDL是Web Service接口的使用说明书,并且W3C为Web Service制定了一套传输类型,使用XML进行描述,即XSD(XML Schema Definition,XML模式定义)​。任何语言编写的Web Service接口在发送数据时,都要转换成Web Service标准的XSD发送。不过鉴于XML文件格式在进行数据交换时会受大小与性能的影响,现在项目中使用Web Service接口的场景已经变得越来越少。

2. HTTP接口

想知道什么是HTTP接口,需要先明确HTTP协议的相关知识。HTTP协议建立在TCP协议之上,当浏览器需要从服务器端获取网页数据时,就会发送一次HTTP请求。HTTP协议会通过TCP协议建立起一个到服务器端的连接通道,当本次请求需要的数据发送完毕后,HTTP协议会立即将TCP协议连接断开,因为这个过程很短,所以HTTP连接不仅是一种无状态的连接还是一种短连接。HTTP协议有很多特点,主要分为以下4个。

  1. 支持客户/服务器模式。
  2. 简单快速:客户端向服务器请求服务时,只需传送请求方式和路径即可。请求方式常用的有GET、POST等,每种方式规定了客户端与服务器联系的类型。HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
  3. 灵活:HTTP协议允许传输任意类型的数据对象,而传输的类型由Content-Type加以标记。
  4. 无状态:HTTP是无状态协议。无状态是指协议对于事务处理没有记忆能力。

HTTP接口采用HTTP协议传输。也就是说,HTTP协议拥有的特点在HTTP接口中都会有所体现。其中最重要的是,HTTP接口是通过路径来区分调用方式的,请求报文都是key-value形式的,返回报文一般是JSON串,相比Web Service接口中的SOAP协议返回较重的XML数据形式,JSON串则更轻,因此在现在的项目开发中,绝大多数项目应用了HTTP接口形式,使用的请求方式是HTTP协议中的GET、POST等。

2.3、正式数据接口与模拟数据接口

前端项目访问的接口主要包括由后端提供的项目正式数据接口和前端人员自行模拟构建的数据接口两大类,下面分别进行介绍。

1. 正式数据接口

当团队后端开发工程师已经成功处理相应功能的接口内容后,通常会给前端提供项目中将要应用的正式数据接口,同时会给出相应的接口文档。不过现在很多的工具可以将接口与文档一同整合,集成一个统一的接口管理应用,比如swagger就是其中一种接口文档生成工具。

尚硅谷电商项目的后端开发工程师提供了一套“尚品汇”电商平台的swagger接口,其地址为“http://39.98.123.211:8510/swagger-ui.html”​,读者可自行查看。

该接口中除了可以进行webApi及adminApi不同项目、不同接口的切换,还罗列了指定项目中提供的众多接口,比如webApi下的订单、购物车、交易、秒杀、评论、退款等接口,如图所示。

在这里插入图片描述

当我们点击某个接口分类时,页面将会展示该接口分类下各个具体接口的详细内容,并且清晰标明该接口的请求方式,比如GET、POST、PUT、DELETE等。这也对应了RESTful API的开发风格,说明swagger接口可以遵循RESTful API的开发标准。以订单接口为例,如图所示。
在这里插入图片描述

其实我们还可以点击具体某个接口的请求方式查看该接口的详情,包括接口的请求地址、请求方式、请求参数和响应体数据结构等,如图所示。

在这里插入图片描述

2. 模拟数据接口

假如当前公司或团队接收了一个全新的项目,这个项目对于产品、设计、前端、开发、测试、运维等技术团队的所有小组成员来说,都是一个从零开始的项目,前端人员在进行Vue项目开发时,后端程序处于一个刚起步的阶段,因此后端人员可能没办法在第一时间给前端人员提供相应的真实调试接口,那么需要如何处理呢?前端人员先休息一段时间,然后在后端接口准备好的情况下回来继续进行项目的开发工作是不现实的。这种情况前端项目与后端接口一定是同步开展的,因此就有了模拟数据接口的需求,让前端人员在暂时没有后端接口的情况下能够顺利地继续进行项目的开发。简单地说,我们需要在没有后端接口支持的情况下,先模拟一些测试用的接口数据。

不过,模拟数据接口的操作流程具体应该是什么样的呢?是否只需要前端人员自己定义一个JSON数据就可以了呢?我们知道操作一个JSON文件可以实现数据的获取,但是只实现数据获取是远远不够的,还需要进行添加、修改、删除等其他操作,也就是RESTful API需要操作的内容。这里简单演示一个模拟数据接口的流程,如图所示。

在这里插入图片描述
除此之外,我们还需要考虑,前端人员自己定义的数据结构后端人员是否知晓,如果后端人员在进行接口开发后,提供的数据结构与前端人员所定义的数据结构有差异,那么这些差异可能会导致前端代码被大量修改(读取数据进行显示的代码、更新数据的代码、读取数据发送请求的代码都有可能被修改)​,这会严重影响开发进度。

此时模拟数据接口的数据结构由谁提供就是一个问题了。如果后端人员提供的特定结构的数据,前端人员不需要又该如何处理呢?

最好的方式是由前后端人员进行无障碍的协调沟通。前端人员如果有很好的主动性,则可以先主动提出具体的接口数据结构需求,然后与后端人员一同探讨,但最终还是需要由后端人员进行确认,敲定模拟数据接口的数据结构内容。

因此,模拟数据接口操作是很有必要的。而在这一过程中,不同岗位之间的沟通与协调也被放置在一个突出的位置。前端人员了解一些后端开发知识,后端人员清楚一些前端开发技术也成了现代开发技术人员的一些更高能力的要求。

上面提到了模拟数据接口的操作,下面通过在线模拟数据接口和本地模拟数据接口两个部分进行讲解。

1. 在线模拟数据接口

根据需求的不同,模拟数据接口也有不同的获取与创建方式。如果现在的项目处于初始阶段,只是为了确认项目中是否可以正常进行数据的增、删、改、查等功能性操作,对数据的结构内容没有过多的要求,那么这时候完全不需要考虑构建模拟数据内容,只需要利用网络上现有的一些在线模拟数据接口平台提供的模拟数据接口就可以实现这一目标。

JSONPlaceholder就是一个应用率非常高的在线免费模拟数据接口平台,平台每个月几十万的请求量得益于它的接口支持RESTful API风格,如图所示。

在这里插入图片描述
JSONPlaceholder在线模拟数据接口的弊端是接口类型少,数据表现形式与项目不一定匹配,甚至请求到的数据内容都是英文示例,对于中文的支持力度几乎没有,而且很难修改其中数据结构的节点属性。那么针对这些弊端应该如何解决呢?答案是:可以尝试配合使用不同的工具。

2. 本地模拟数据接口

本地模拟数据接口需要使用json-server和Mock.js,这两个名词在本书中初次出现,那么什么是json-server和Mock.js呢?

json-server是一个号称不到30秒就可以获得零编码的模拟数据接口的工具,并且它还是一个支持RESTful API风格的工具;Mock.js是一个可以随机生成数据并且可以拦截AJAX请求的工具。这样来看,一个是获得数据的工具,另一个是生成数据的工具,配合使用二者就可以解决JSONPlaceholder在线模拟数据接口的弊端,实现数据的生成和获取。下面就对这两个工具分别进行讲解,并配合使用。

1. json-server

详情如下:json-server

2. Mock.js

在程序开发和调试阶段,大多数开发人员往往不太重视数据的真实性。例如,姓名、地址、电话等数据,经常会输入“aaa”​“bbb”​“123”来调试和展示。这经常会导致整体应用和最终的实际效果相差甚远,从而需要耗费更多的时间和精力,有些得不偿失。而Mock.js完美地解决了这个痛点。

在使用Mock.js之前,先在目录中安装Mock.js模块。在终端中输入下方命令。

npm install mockjs --save

然后新建一个index.js文件,在该文件中引入Mock.js模块,并尝试通过此模块随机生成一条假数据。

let Mock = require('mockjs')
console.log(Mock.Random.cname())

打开终端运行如下命令。

npm index.js

运行命令后可以发现,每次运行该命令输出的信息都是不同的,由此可见,通过Mock.js可以生成一些贴近实际应用的假数据。

使用Mock.js可以生成一条数据,那么想要生成多条数据应该如何处理呢?

其实完全可以通过循环的方式来实现这个效果。例如,现在的需求是构建一批用户数据,就可以在上面index.js文件代码的基础上进行修改。

修改后的json-server-mock-server/index.js文件代码如下。

let Mock = require('mockjs')
console.log(Mock.Random.cname())

for(let i = 0; i < 58; i++) {
    console.log(Mock.Random.cname())
}

依旧运行命令“node index.js”​,运行后可以发现生成了多条数据,并且没有一条数据是重复的。这里只截取部分数据,如图所示。

郝刚
徐芳
龙杰
郝娟
潘秀兰
白杰
郑秀兰
汤洋
谢敏
方明
钱静
高磊
蔡超
沈杰
金艳

虽然已经成功地使用Mock.js生成了一系列的随机数据,但是似乎现在与json-server本地服务接口还没有任何的关联。如果按照原来json-server的操作模式,则要把Mock.js生成的数据直接复制/粘贴到db.json文件中,但这个操作过程过于烦琐。最好的方式就是让Mock.js与json-server建立合作渠道,从而方便数据的应用。

接下来就在index.js中引入Mock.js,并且利用其内置方法Mock.Random随机生成数据。值得一提的是,在生成数据时,我们可以利用module.exports函数定义模块,并且通过定义内容为函数的形式暴露模块。读者可能会疑惑,当前的环境是Mock.js与json-server,而module.exports函数是Node环境下CommonJS的规范,为什么还能直接使用呢?这是因为当前程序的运行环境本质上的底层基础就是Node,这与CommonJS的规范是重合的,所以在当前环境下也是可以使用module.exports函数的语法的。

在module.exports函数中创建一个对象类型的data,并且包含一个名为users的属性节点。利用循环向这个data下的users数组追加内容,最终将data数据返回。这部分的内容比较简单,可以理解为函数定义和随机内容的生成,只不过相比原来的操作多了一个模块暴露,而这个暴露的操作主要是让json-server可以应用当前的模拟数据接口模块。

修改后的index.js文件代码如下。

let Mock = require('mockjs')
let Random = Mock.Random

module.exports = ()=>{
    let data = {
        users: []
    }

    for(let i = 1; i <= 324; i++) {
        data.users.push({
            id: i,
            name: Random.cname(),
            address: Random.cword(10, 20),
            avatar: Random.image('100x100', Random.color(), '#FFF', Random.name())
        })
    }
    return data;
}

将之前运行db.json文件的终端命令终止,运行新的命令,让index.js文件替换db.json文件。

json-server -p 3000 index.js

运行完毕后,打开浏览器,在地址栏中输入“http://localhost:5000”​,就可以看到users进入了json-server本地服务接口中。

在这里插入图片描述

3、原生API请求

3.1、AJAX基本概念与操作步骤

AJAX(Asynchronous JavaScript and XML),即异步JavaScript和XML。通过AJAX可以在浏览器中向服务器发送AJAX请求。AJAX具有很多种优势,比如,无须刷新页面即可与服务器端进行通信、允许根据用户事件更新部分页面内容等,但它也具有一些劣势,比如没有浏览历史、不能回退、存在AJAX跨域请求问题、对SEO不友好、爬虫爬取不到网页中的内容等。

使用AJAX进行数据请求,需要经过如下5个主要操作步骤。

  1. 创建XMLHttpRequest的实例对象。
  2. 初始化请求,指定请求方式和请求地址。
  3. 给实例对象绑定readyStateChange事件监听,用于读取AJAX请求的响应结果数据。
  4. 发送请求。
  5. 当请求完成后,如果请求成功,则读取响应结果数据并处理;如果请求失败,则提示错误信息。

那么在Vue项目中如何认证AJAX操作的步骤呢?

利用vite创建一个请求项目,将项目的名称设置为“vue3-book-request”​,命令如下。

npm vreate vite@latest vue3-book-request -- --template vue

首先将App.vue根组件文件中的代码内容清除,然后直接按照上面描述的AJAX操作步骤实现即可。此时App.vue文件代码如下。

<script setup>
//第一步:创建XMLHttpRequest的实例对象
const xhr = new XMLHttpRequest()
//第二步:初始化请求,指定请求方式和请求地址
const url = 'http://39.98.123.211:8510/api/cms/banner'
xhr.open('GET', url)
//第三步:给实例对象绑定readyStateChange事件监听,用于读取AJAX请求的响应结构数据
xhr.onreadystatechange = ()=>{
  /*
    第五步:当请求完成后,如果请求成功,则读取响应结果数据并处理;如果请求失败,则提示错误信息
    readyState 0 请求未初始化 刚刚实例化XMLHttpRequest
    readyState 1 客户端与服务器端建立连接 调用open方法
    readyState 2 请求已经被接收
    readyState 3 请求正在处理中
    readyState 4 请求完成
  */
  //当请求完成时
  if(xhr.readyState === 4) {
    /*
      status是响应状态码,有2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务器端错误)
      responseText是服务器端返回的响应体文本内容
    */
   //如果请求成功,则解析响应体JSON数据为JavaScript对象,并在控制台输出
   if(xhr.status >= 200 && xhr.status < 300) {
    const result = JSON.parse(xhr.responseText)
    console.log(result)
   }else{
    alert(`请求失败,错误信息为:${xhr.statusText}`)
   }
  }
}
//第四步:发送请求
xhr.send()
</script>

<template>
  <div>App</div>
</template>

不过现在运行项目后,并没有成功请求到接口数据,而是在控制台中报出了错误信息。错误信息的大致意思是:http://127.0.0.1:5173的项目地址想去请求http://39.98.123.211:8510/api/cms/banner的接口地址产生了违反CORS(Cross-Origin ResourceSharing,跨域资源共享)同源策略的AJAX跨域请求问题。

那么我们在进行项目开发时应如何解决AJAX跨域请求问题呢?其实是有多种解决方案的,这里就不再一一列举了。下面使用Vue项目开发中常用的解决方案——配置代理服务器来解决AJAX跨域请求问题。代理服务器的基本思想就是:浏览器发送同源请求(请求当前项目下的地址)​,由代理服务器将请求转发到跨域的目标服务器上,代理服务器得到响应后,自动传递给浏览器。这就实现了浏览器本质上发送的是同源请求,而请求到的是跨域服务器的接口,从而解决AJAX跨域请求问题。

使用vite创建的项目,其内部已经集成了代理服务器,我们只需做适当的配置即可。下面分两步实现。

第1步:在vite.config.js文件中配置代理服务器,修改后的vite.config.js文件代码如下。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      //只穿法请求路径以'/apPrefix'为前缀的请求
      '/apiPrefix': {
        target: 'http://39.98.123.211:8510/api',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/apiPrefix/, '')
      }
    }
  }
})

第2步:修改组件中AJAX请求的路径,将跨域路径修改为当前项目下的路径,也就是将“http://39.98.123.211:8510/api/cms/banner”修改为“/apiPrefix/cms/banner”​。需要注意的是,不写基础路径时,就是请求当前项目的运行路径(同源请求)​;路径开头部分必须是“/apiPrefix”​,这样代理服务器才能匹配到,从而转发跨域请求的对应接口。App.vue代码修改如下。

//第二步:初始化请求,指定请求方式和请求地址
// const url = 'http://39.98.123.211:8510/api/cms/banner'
const url = '/apiPrefix/cms/banner'

最后,我们可以对AJAX请求进行相对简单的封装。定义一个相对通用的函数,接收请求的url参数,利用Promise的实例对象来封装AJAX请求,如果请求成功,则返回一个成功的promise对象;如果请求失败,则返回一个失败的promise对象。修改完成后的App.vue文件代码如下。

<script setup>
  import {ref, onMounted} from 'vue'

  //AJAX请求函数
  function ajax(url) {
    //利用Promise的实例对象来封装AJAX请求
    return new Promise((resolve, reject)=>{
      const xhr = new XMLHttpRequest()
      xhr.open('GET', url)
      xhr.onreadystatechange = ()=>{
        //当请求完成时
        if(xhr.readyState == 4) {
          //如果请求成功,则解析响应体JSON,并指定为Promise成功的value
          if(xhr.status >= 200 && xhr.status < 300) {
            const result = JSON.parse(xhr.responseText)
            resolve(result)
          } else {
            //如果请求失败,创建包含错误信息的error对象,并指定为PRomise失败的reason
            reject(new Error(`请求失败,错误信息:${xhr.statusText}`))
          }
        }
      }
      xhr.send()
    })
  }

  //列表数据
  const list = ref([])
  //初始化挂载回调
  onMounted(()=>{
    //获取列表数据显示
    ajax('/apiPrefix/cms/banner')
      .then(value=>{
        list.value = value.data
      })
      .catch(error=>{
        alert(error.message)
      })
  })
</script>

<template>
  <div class="app">
    <div class="item" v-for="item in list" :key="item.id">
      <span>{{ item.title }}</span>
      <img src="item.imageUrl" alt/>
    </div>
  </div>
</template>

<style scoped>
  .item {
    height: 10px;
    margin: 10px;
    display: flex;
    align-items: center;
  }
  .item span {
    display: inline-block;
    width: 150px;
  }
  .item img {
    height: 10px;
    width: 100px;
  }
</style>

运行项目后就可以正常访问和显示接口内容了,如图所示。
在这里插入图片描述

3.2、fetch请求

AJAX请求函数的封装只是实现了最基本的功能,仅仅抽离url为动态参数,像method请求方式、headers请求头、Content-Type设置等内容都没有实现动态化抽离,后续想要实现完整、强大的自定义AJAX还是比较烦琐的。此时可以考虑浏览器自带的fetch请求函数。

fetch是一种可以代替XMLHttpRequest的HTTP数据请求方式。需要注意的是,fetch不是对AJAX的进一步封装,它们是两种数据请求方式,fetch函数就是原生JavaScript,并没有使用XMLHttpRequest对象。而XMLHttpRequest是一个设计粗糙的API,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有Promise、async和await友好。

fetch的出现就是为了解决XMLHttpRequest的问题的,它实现了Promise规范,返回Promise的实例对象,而Promise是开发者为解决异步回调问题而推出的一套方案。

此时将App.vue文件中的代码改写为下方代码。

<script setup>
  import {ref, onMounted} from 'vue'

  //列表数据
  const list = ref([])
  //初始化挂载回调
  onMounted(()=>{
    //调用fetch函数发送AJAX请求获取列表数据显示
    fetch('/apiPrefix/cms/banner')
      .then((response)=>{
        return response.json()
      })
      .then((data)=>{
        list.value = data.data
      })
      .catch((error)=>{
        alert(error.message)
      })
  })
</script>

因为fetch就是ES6(ECMAScript6)提供的一个异步接口,所以基本的fetch操作很简单,就是通过fetch请求,返回一个Promise的实例对象,在Promise实例的then方法里面用fetch的response.json(​)等方法解析数据。由于这个解析返回的也是一个Promise的实例对象,因此需要使用两个then方法才能得到我们需要的JSON数据。

虽然fetch具有诸多优势,比如浏览器内置、代码语法简洁、更加语义化、基于标准Promise实现、支持async、await等,但它也具有很多劣势,比如不支持文件上传进度监测、默认不带cookie、不支持请求终止、没有统一的请求响应拦截器、使用不完美、需要大量的功能封装等,这对于开发者来说操作依旧很烦琐。

4、axios请求

4.1、axios基本请求实现

在实际项目开发中,我们一般不会使用原生XMLHttpRequest对象来发送AJAX请求与后端接口进行交互,而是使用第三方库axios来编写请求代码,甚至会对axios进行二次封装来简化请求的代码。

axios是一个具有独立开发功能、目标明确的请求库。它基于Promise,是一个既可以用于浏览器又可以用于Node服务器的HTTP请求模块。本质上它是符合最新ES规范使用Promise实现的原生XHR的封装。在服务器端它使用原生Node的HTTP模块实现,而在客户端则使用XMLHttpRequests实现。因为axios的作者对于请求操作已经在axios第三方库中进行了常用功能的封装,所以开发人员想实现网络请求功能只需直接安装并使用它即可,其拥有诸多特性,主要包括以下几点。

  • 支持从浏览器创建XMLHttpRequests。
  • 支持从Node创建HTTP请求。
  • 支持Promise API。
  • 拦截请求和响应。
  • 转换请求和响应数据。
  • 取消请求。
  • 自动转换JSON数据。
  • 客户端支持防御XSRF。

在vue3-book-request项目中安装axios,命令如下。

npm install axios --save

转换为axios代码模式,此时App.vue文件代码如下。

<script setup>
  import {ref, onMounted} from 'vue'
  import axios from 'axios'

  //列表数据
  const list = ref([])
  //初始化挂载回调
  onMounted(()=>{
    //调用axios的get方法发送AJAX请求获取列表数据显示
    axios.get('apiPrefix/users')
      .then((response)=>{
        console.log(response)
        const result = response.data
        list.value = result
      })
      .catch((error)=>{
        alert(error.message)
      })
  })
</script>

<template>
  <div class="app">
    <div class="item" v-for="item in list" :key="item.id">
      <span>{{ item.id }}</span>
      <span>{{ item.name }}</span>
    </div>
  </div>
</template>

<style scoped>
  .item {
    height: 10px;
    margin: 10px;
    display: flex;
    align-items: center;
  }
  .item span {
    display: inline-block;
    width: 150px;
  }
  .item img {
    height: 10px;
    width: 100px;
  }
</style>

在这里插入图片描述

4.2、axios项目功能集成

不管是XMLHttpRequest、fetch还是axios,在实际的复杂项目开发中通常都不会直接请求,而是会进一步封装请求,以简化后续出现的代码重用。接下来将以axios库为例,结合json-server与Mock.js模拟出来的接口数据进行实际项目开发中请求的代码封装应用操作。

在实现axios库二次封装前,先来讲解一下axios库的几个重要语法。

1. 创建新的axios函数
axios库默认暴露的只有一个函数axios,我们执行axios函数可以发送AJAX请求,同时axios函数提供了一些发送不同类型的AJAX请求的静态方法,如get、post、put、delete等。通过6.5.1节中组件的多个请求代码,读者应该能发现,每次指定请求地址时都需要携带一个基础路径“http://localhost:5000”​,我们可以通过“axios.defaults.baseURL=‘http://localhost:5000/’”进行统一指定,这样在组件中就不再需要指定这个基础路径了。

现在,我们假设当前前台应用还需要请求另一台服务器上的接口,它的基础路径、请求超时时间、请求头等都可能与用户接口不同。此时用一个axios函数就不能实现了,我们可以利用axios函数提供的create静态方法来创建一个新的axios函数,并指定新的baseURL。axios.create的语法如下。

axios.create([])

在config对象中,我们可以指定特定的baseURL(基础路径)​、timeout(请求超时时间)等。

2. 使用axios拦截器

使用axios库来请求数据的一个非常重要的原因就是它有一项功能叫拦截器(Interceptors),这也是fetch所没有的。如果想要使用fetch实现拦截器功能,就需要编写大量代码进行自定义的封装实现,或者安装一个类似fetch-intercept的第三方库,而使用axios拦截器就可以一步到位解决这个问题。下面就来介绍axios拦截器的相关知识。

axios拦截器分为两种,分别是请求拦截器和响应拦截器。

  1. 请求拦截器:在请求发送前进行必要的操作处理,例如,添加统一cookie、添加请求验证、设置请求头等,相当于对每个接口中相同操作的一个封装。配置请求拦截器的语法如下。
axios.interceptors.request.use()
  1. 响应拦截器:功能与请求拦截器的功能基本相似,只不过响应拦截器是在请求得到响应后,对响应体进行处理,通常是用来对数据进行统一处理等,也常用来判断登录是否失效等。配置响应拦截器的语法如下。
axios.interceptors.response.use()

这里将二者的区别通过图片来展示,如图所示。

在这里插入图片描述

从图中可以看出,组件A发起请求,在到达API服务器之前的这个过程中请求拦截器就会生效,也就是说,如果我们想要添加统一cookie,或者设置请求头就可以在这里执行;当API服务器发出响应以后,会将响应内容返回组件中,在这个过程中响应拦截器就会生效,也就是说,我们可以在这里对数据进行统一处理,将处理过的数据返回组件中。

例如,一些网站在一定时间后没有被操作,就会自动退出登录,让用户重新登录。想要实现这样的功能,如果不用拦截器则操作起来会很麻烦,而且从代码上来说也会深度冗余,如果使用拦截器解决这个问题,则操作会变得更加简单。

axios拦截器的作用强大,每个axios函数都可以设置多个请求拦截器或者响应拦截器,同时每个拦截器都可以设置两个拦截函数,分别用于成功拦截和失败拦截。在调用axios函数之后,请求流程会先进入请求拦截器中,在正常情况下,可以一直执行请求成功拦截函数;如果有异常,则会执行请求失败拦截函数,但这个时候并不会发起请求。当请求返回后,请求流程会根据响应信息进入响应拦截器中,执行响应成功拦截函数或者响应失败拦截函数。

下面简单演示拦截器的配置,代码如下。


  //添加请求拦截器
  axios.interceptors.request.user(
    //请求前成功的拦截器
    (config)=>{
      return config
    },
    //请求前失败的拦截器,一般很少用
    (error)=>{
      return Promise.reject(error)
    }
  )
  //添加响应拦截器
  axios.interceptors.response.user(
    //成功响应的拦截器
    (response)=>{
      return response
    },
    //失败响应的拦截器
    (error)=>{
      return Promise.reject(error)
    }
  )

下面对axios库进行二次封装。

可以利用axios库的create方法来创建一个新的axios函数,同时封装特定的基础路径和请求超时时间。可以利用axios库的请求拦截器语法,实现在发送请求前对请求进行统一处理,比如向请求头中添加需要携带给后台接口的token数据。可以利用axios库的响应拦截器语法,实现在请求成功或失败返回后,进行请求成功或失败的统一处理,比如,在请求成功后,返回页面需要的响应体数据;在请求失败后,给予统一的请求失败提示。

创建axios库的二次封装模块文件api/jsonServerAxios.js,代码如下。

import axios from 'axios';

//创建针对json-server提供的接口的axios
const jsonServerAxios = axios.create({
    baseURL: 'http://localhost:3000', 
    timeout: 1000 * 20,
    headers: {
        'X-Custom-Header': 'custom value'//在header头部信息中自定义头信息设置
    }
})

//模拟保存登录请求返回的tocken到LocalStorage中
localStorage.setItem('TOKEN_KEY', '123456')

//添加请求拦截器
jsonServerAxios.interceptors.request.use(
    (config)=>{
        //读取LocalStorage中的token数据,如果token数据被添加到请求头中
        const token = localStorage.getItem('TOKEN_KEY')
        if(token) {
            config.headers['token'] = token
        }
        //返回配置
        return config
    },
    (error)=>{
        return Promise.reject(error)
    }
)

//添加响应拦截器
jsonServerAxios.interceptors.response.use(
    (response)=>{
        /*
            请求能够正常响应不代表一定能够获取服务器返回的数据。
            有时候请求成功服务器却会因为具体的业务场景返回一定的错误编码与数据信息。
            
            可以返回指定对象,报空响应状态码、状态提示文本、响应体数据内容
            return {
                code: response.status,
                message: response.statusText,
                data: response.data
            }
        */
       return response.data
    },
    (error)=>{
        if(!error.response) {
            alert('网络连接不上,请检查网络')
        } else if (error.response) { //根据响应状态码,设置不同的错误提示信息
            switch(error.response.status) {
                case 400:
                    error.message = '错误请求';
                    break;
                case 401:
                    error.message = '未授权,请重新登录';
                    break;
                case 403:
                    error.message = '拒绝访问';
                    break;
                case 404:
                    error.message = '请求错误,未找到该资源';
                    break;
                case 405:
                    error.message = '请求方式未允许';
                    break;
                case 408:
                    error.message = '请求超时';
                    break;
                case 500:
                    error.message = '服务器端输出';
                    break;
                case 501:
                    error.message = '网络未实现';
                    break;
                case 502:
                    error.message = '网络错误';
                    break;
                case 503:
                    error.message = '服务不可以';
                    break;
                case 504:
                    error.message = '网络超时';
                    break;
                case 505:
                    error.message = 'http版本不支持该请求';
                    break;
                default:
                    error.message = `连接错误${error.response.status}`
            }
            //对错误提示信息的处理可以统一放在这里,如页面提示错误等
            alert(error.message)
        }
        return Promise.reject(error)
    }
)

axios库二次封装模块实现后,基于当前模块,下一步可以为请求接口封装相应的接口请求函数,并将同一资源的不同操作的多个接口对应的接口请求函数封装在一个模块文件中。当然一个项目中很可能是有不同资源的接口的,需要创建多个包含n个接口请求函数的模块。当然,当前只有一个通过json-server创建的users接口,它包含下面5个接口。

  1. 通过用户id获取对应的用户信息。
  2. 获取用户列表。
  3. 添加用户。
  4. 修改用户。
  5. 删除用户。

可以定义一个包含对应的5个接口请求函数的模块,创建模块文件api/usersApi.js,代码如下。

//引入jsonServerAxios
import jsonServerAxios from './jsonServerAxios'

//通过用户id获取对应的用户信息
export const reqGetUser = async (id) => jsonServerAxios.get(`/users/${id}`)

//获取用户列表
export const reqGetUserList = async (params) => jsonServerAxios.get('/users', {params})

//添加用户
export const reqAddUser = async (user) => jsonServerAxios.post('/users', user)

//修改用户
export const reqUpdateUser = async (user) => jsonServerAxios.put(`/users/${user.id}`, user)

//删除用户
export const reqRemoveUser = async (id) => jsonServerAxios.delete(`/users/${id}`)

接口请求函数封装好之后,开发者就可以在任意组件中引入并调用这些接口请求函数来实现数据操作的相应功能了。当前可以在App组件中实现用户的增、删、改、查一系列操作,例如,我们可以在App组件中引入usersApi模块中提供的5个接口实现用户的增、删、改、查,包括添加用户、更新用户、删除用户、查看用户详情、获取用户列表,代码如下。

<script setup>
  import {onMounted, ref} from 'vue'
  //引入接口
  import {
    reqGetUserList,
    reqAddUser,
    reqUpdateUser,
    reqRemoveUser,
    reqGetUser
  } from './api/usersApi'
  const users = ref([])
  const error = ref('')

  //添加用户
  const addUser = async ()=>{
    const user = {
      name: 'tom',
      address: '北京',
      avatar: 'http//dummyimage.com/100x100/aaa'
    }
    //请求新增用户
    await reqAddUser(user)
    //成功后,重新获取用户列表
    getUsers()
  }

  //更新用户
  const updateUser = async (id)=>{
    //指定更新信息的用户对象
    const user = {
      id,
      name: 'tom',
      addresss: '修改后',
      avatar: 'http//dummyimage.com/100x100/aaa'
    }
    //请求修改用户
    await reqUpdateUser(user)
    //成功后,重新获取用户列表
    getUsers()
  }

  //删除用户
  const deleteUser = async (id)=>{
    //请求删除用户
    await reqRemoveUser(id)
    //成功后,重新获取用户列表
    getUsers() 
  }

  //查看用户详情
  const viewUserById = async (id)=>{
    //请求获取用户信息
    const result = await reqGetUser(id)
    //成功后,得到用户信息并显示
    const user = result
    alert(JSON.stringify(user))
  }

  //获取用户列表
  const getUsers = async ()=>{
    try {
      //请求获取用户的分页列表
      const result = await reqGetUserList({
        _page: 1, 
        _limit: 20,
        _sort: 'id',
        _order: 'desc'
      })
      //成功后,读取得到的用户列表,更新状态并显示
      users.value = result
    } catch (error) {
      error.value = error.message
    }
  }

  //初始化用户列表
  onMounted(()=>{
    getUsers()
  })
</script>

<template>
  <div v-if="error">error:{{ error }}</div>
  <ul>
    <li v-for="user in users" :key="user.id">
      <span class="item-id">{{ user.id }}</span>
      <span class="item-name">{{ user.name }}</span>
      <a href="javascript:void(0)" @click="updateUser(user.id)">修改</a>
      &nbsp;
      <a href="javascript:void(0)" @click="deleteUser(user.id)">删除</a>
      &nbsp;
      <a href="javascript:void(0)" @click="viewUserById(user.id)">查看详情</a>
    </li>
  </ul>
  <button @click="addUser">新增用户</button>
</template>

<style scoped>
  .item {
    height: 10px;
    margin: 10px;
    display: flex;
    align-items: center;
  }
  .item-id {
    display: inline-block;
    width: 100px;
    margin-right: 10px;
  }
  .item-name {
    display: inline-block;
    width: 100px;
  }
</style>

修改后的vite.config.js文件代码如下。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      //只穿法请求路径以'/apPrefix'为前缀的请求
      '/apiPrefix': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/apiPrefix/, '')
      }
    }
  }
})

在这里插入图片描述

有一个重要问题需要单独说明一下,上面各个操作对应的请求都是跨域AJAX请求,前台并没有做任何处理,但请求都被正常处理了,这是因为json-server创建的服务器接口已经在服务器端通过返回特定响应头的方式解决了AJAX跨域请求问题。如果后台接口没有进行处理呢?在项目中就需要通过配置代理服务器来解决此问题​,只需要两步就可以解决。

第1步:在vite.config.js中配置代理服务器,也就是在proxy配置中添加一个代理配置,代码如下。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      //只穿法请求路径以'/apPrefix'为前缀的请求
      '/apiPrefix': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/apiPrefix/, '')
      }
    }
  }
})

第2步:修改axios库二次封装的模块文件jsonServerAxios.js中的baseURL配置的基础路径,将其修改为“/jsonPrefix”​。

重新启动项目,功能与之前是一致的。

当然这套接口本身是不需要配置代理服务器的,这里只是为了演示说明。

更多推荐