异步请求ajax/fetch/axios学习笔记__李立超课程

129 阅读5分钟

异步请求

  • 我们之前编写的服务器都是传统的服务器,服务器的结构是基于MVC模式

    • MVC模式:
      • Model----数据模型 (data)
      • View----视图,用来呈现 (views)
      • Controller----控制器,加载数据并选择视图来呈现数据 (router)
    • 传统的服务器是直接为客户端返回一个页面,但是传统的服务器并不能适用于现在的应用场景
  • 现在的应用场景,一个应用通常都会有多个客户端(client)存在

    web端移动端(app)pc端
    • 如果服务器直接返回html页面,那么服务器就只能为web端提供服务,其他类型的客户端还需要单独开发服务器,提高了开发和维护的成本

    • 传统的服务器需要做两件事情

      1. 加载数据
      2. 要将模型渲染进视图
    • 如何解决这个问题?

      • 将渲染视图的功能从服务器中剥离出来,服务器只负责向客户端返回数据,渲染视图的工作由客户端自行完成

      • 分离以后,服务器只提供数据,一个服务器可以同时为多种客户端提供服务,同时将视图渲染的工作交给客户端以后,简化了服务器代码的编写

  • 实现异步请求的方案

    1. ajx:XMLHTTPRequest(xhr)
    2. Fetch
    3. Axios

Rest

  • REpresentational State Transfer,是一种针对网络应用程序设计的架构风格,它采用统一的接口原则进行资源的访问和操作。

  • 主要特点:服务器只返回数据。服务器和客户端传输数据时通常会使用JSON作为数据格式

  • 请求的方法
    GET加载数据(查询统一用get)
    POST新建或添加数据
    PUT添加或修改数据
    PATCH修改数据
    DELETE删除数据
    OPTION由浏览器自动发送,检查请求的一些权限
    • RESTful API(接口)定义了一组用于与Web服务进行通信的规则和约定。 (Endpoint端点)

      GET /user

      POST /user

      DELETE /user/:id ...

  • 统一的api:路由路径统一、发送数据格式统一

CORS:跨域资源共享

  • 跨域检查:1. 协议 2. 域名 3. 端口号 三个只要有一个不同,就算跨域

  • 当通过AJAX去发送跨域请求时,浏览器为了服务器的安全,会阻止JS读取到服务器的数据

  • 解决方案:在服务器中设置一个允许跨域的头,Access-Control-Allow-Origin,允许那些客户端访问我们的服务器

    • Access-Control-Allow-Origin 设置指定值时只能设置一个

      res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500")

服务器端:

// 解析json格式请求体的中间件
app.use(express.json())

app.use((req, res, next) => {
    // 设置响应头
    res.setHeader("Access-Control-Allow-Origin", "*")
    // Access-Control-Allow-Methods 允许的请求的方式
    res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH")
    // Access-Control-Allow-Headers 允许传递的请求头
    res.setHeader("Access-Control-Allow-Headers", "Content-type")
    
    next()
})

Ajax

  • Ajax(Asynchronous JavaScript and XML):在js中向服务器发送请求和加载数据的技术叫AJAX。通过 AJAX 可以在浏览器中向服务器发送异步请求,最大的优势:无刷新获取数据

  • XML可扩展标记语言:早期AJAX使用的数据格式,目前数据格式都使用json

  • 使用方法

btn.onclick = () => {
    // 创建一个xhr对象
    const xhr = new XMLHttpRequest()

    // 设置响应体的类型,设置后会自动对数据进行类型转换
    xhr.responseType = "json"

    // 可以为xhr对象绑定一个load事件,当xhr加载完毕后才执行函数(异步)
    xhr.onload = function () {
        // xhr.status 表示响应状态码 / 服务器中设置的status是指数据传递的状态
        if (xhr.status === 200) {         
            //手动转换响应的类型 等价于 xhr.responseType = "json"
            // const result = JSON.parse(xhr.response) 
            // console.log(result.status, result.data)
            
            // xhr.response 表示响应信息 前面设置响应为json,此时会自动转换成对象类型
            const result = xhr.response 
            if (result.status === "ok") // 判断数据是否正确
            {
                const ul = document.createElement("ul")
                root.appendChild(ul)
                for (let stu of result.data) {
                    ul.insertAdjacentHTML(
                        "beforeend",
                        `<li>${stu.id} - ${stu.name} - ${stu.age} - ${stu.gender} - ${stu.address}</li>`
                    )
                }
            }
        }
    }

    // 设置请求的信息
    xhr.open("get", "http://localhost:3000/students")
    // 发送请求
    xhr.send()
}

fetch

  • fetch是xhr的升级版,采用的是Promise API,作用和AJAX一样,是原生js就支持的一种异步请求的方式

  • 用fetch获取到的数据是服务器端返回的数据

  • fetch的配置对象:method......

    fetch("http://localhost:3000/students", 
      {
          method: "post", //使用不同请求时,需载服务器端允许接收不同请求
          headers:{
              "Content-type":"application/json"
          },
          // 通过post的请求体body去发送数据时,必须通过请求头来指定数据的类型
          body: JSON.stringify({
              //content
          })
    })
    
  • fetch的请求中止:使用AbortController

    let controller
    
    btn01.onclick = () => {
        // 1. 创建一个AbortController
        controller = new AbortController()
        
        //2. 设置fetch的配置对象
        fetch("http://localhost:3000/test", {
            signal: controller.signal
        })
            .then((res) => console.log(res))
            .catch((err) => console.log("出错了", err))
    }
    
    btn02.onclick = () => {
        //3. 调用请求中止
        controller && controller.abort()
    }
    
  • fetch的await使用:将promise改写为await时,一定要写try-catch

    btn03.onclick = async () => {
        try {
            const res = await fetch("http://localhost:3000/students")
            const data = await res.json()
        } catch (e) {
            console.log("出错了", e)
        }
    }
    
    fetch("http://localhost:3000/students")
      .then((res) => { //这一步获取到服务器端响应的对象
          if(res.status === 200){
              return res.json() //res.json() 可以用来读取json格式的数据,此时res.json()是一个promise
          }else { throw new Error("加载失败!") }
      })
      .then(res => { //这一步获取到服务器端传回的数据,此时直接是对象形式
          if(res.status === "ok"){
              // 对数据进行操作
          }
      })
      .catch((err) => {
          console.log("出错了!", err)
      })
    

本地存储

  • 问题:在创建登录页面后,登陆成功后只要一刷新就会跳转到重新登录页面,需要解决这个问题,需要将用户信息存储到本地中。cookie和session不能用因为跨域实现。
  • 本地存储就是指浏览器自身的存储空间,可以将用户的数据存储到浏览器内部

  • 类型

    • sessionStorage中存储的数据,页面一关闭就会丢失
    • localStorage存储的时间比较长,只有被显式地删除或者浏览器清除缓存时才会丢失
  • 方法:localStorage.setItem("name", "孙悟空")

    setItem() 存储数据getItem() 获取数据removeItem() 删除数据clear() 清空数据

token

  • 问题:登录以后直接将用户信息存储到了localStorage。 1. 数据安全问题 2. 服务器没有验证登录
  • 使用token解决:

    1. 对数据加密(token)
    2. 告诉服务器客户端的登录状态
      • rest风格的服务器是无状态的服务器,服务器中不能存储用户信息,但可以将用户信息发送给客户端保存
      • 客户端每次访问服务器时,直接将用户信息发回,服务器就可以根据用户信息来识别用户的身份(解密成功则有权限)
  • jsonwebtoken:jwt --> 通过对json加密后,生成一个web中使用的令牌

    • jwt.sign():加密
    • jwt.verify():解密
    // 安装后引入jwt
    const jwt = require("jsonwebtoken")
    
    // 创建一个对象
    const obj = {name: "max"}
    
    // 使用jwt来对json数据进行加密
    const token = jwt.sign(obj, "hihihihihi", {
        expiresIn: "1" //有效时长
    })
    try {
        //服务器收到客户端的token后
        const decodeData = jwt.verify(token, "hihihihihi")
    } catch (e) {
        // 说明token解码失败,说明token
        console.log("无效的token")
    }
    

axios

  • 可以理解为对xhr的封装。和fetch一样,都可以在浏览器和nodejs中运行
  • 网页端直接在html中引入:<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

配置对象

  • baseURL 指定服务器的根目录(路径的前缀)

  • url 请求地址

  • method 请求方法,默认是get

  • data 请求体 {a : "xxx"} / "a=xxx&b=xxx"

  • params 用来指定路径中的查询字符串

  • timeout 过期时间

  • transformRequest 可以用来处理请求数据(data)

    • 它需要一个数组作为参数,数组可以接收多个函数,请求发送时多个函数会按照顺序执行,函数在执行时,会接收到两个参数data和headers
    // axios(config)
    axios({
        method: "post",
        url: "http://localhost:3000/students",
        data:{}
        params:{
            id:1,
            name:"swk"
        },
        transformRequest:[function(data, headers){
            // 可以在函数中对data和headers进行修改
            data.name = "xxx"
            headers["Content-Type"] = "application/json"
            return data
        }, function(data, headers){
            return JSON.stringify(data) // 最后一个函数必须返回一个字符串,才能使得数据有效
        }]
        // 指定请求头 axios会根据内容自动选择类型
        // headers:{"Content-type":"application/json"}
    })
        .then((result) => {
        	//axios默认只会在响应状态为2xx时才会调用then
            // result是axios封装过的对象
        })
        .catch((err) => {
            console.log("出错了!", err)
        })
    }
    

默认配置

设置默认配置后,后面所有请求的该配置都遵循默认配置

axios.defaults.baseURL = "http://localhost:3000"
axios.defaults.headers.common["Authorization"] = `Bearer ${localStorage.getItem("token")}`

axios实例

  • axios实例相当于是axios的一个副本,它的功能和axios一样

  • axios的默认配置在实例也同样会生效,但可以单独对实例的默认配置进行修改

  • 创建实例:const instance = axios.creat()

    axios.defaults.baseURL = "http://localhost:3000"
    axios.defaults.headers.common["Authorization"] = `Bearer ${localStorage.getItem("token")}`
    
    const instance = axios.create({
        baseURL:"http://localhost:4000"
    }) //创建实例监听另一个端口
    
    const instance = axios.create()
    instance.defaults.baseURL = "xxx"
    

拦截器

  • axios的拦截器有请求拦截器和响应拦截器,在请求发送前和响应读取前处理数据

  • 拦截器只对当前的实例有效(给哪个开启拦截器,就对哪个生效)

    axios.interceptors.request.use(
        function (config) {
            // config 表示axios中的配置对象
            // 可以修改对象
            config.headers["Authorization"] = `Bearer ${localStorage.getItem("token")}`
            // 在发送请求之前做些什么
            return config
        },
        function (error) {
            // 对请求错误做些什么
            return Promise.reject(error)
    })
    
    //添加响应拦截器
    axios.interceptors.response.use(
        function (response) {
        // 2xx 范围内的状态码都会触发该函数。
        // 对响应数据做点什么
        return response;
        }, function (error) {
            // 超出 2xx 范围的状态码都会触发该函数。
            // 对响应错误做点什么
            return Promise.reject(error);
    });
    

fetch例子

登录界面,登陆成功后可访问数据

客户端

<!DOCTYPE html>
<html lang="zh">
    <head>
        <meta charset="UTF-8" />  ......
        <style>
            table {
                border-collapse: collapse;
                width: 50%;
            }
            td,
            th {
                font-size: 20px;
                text-align: center;
                border: 1px solid #000;
            }
            caption {
                font-size: 30px;
                font-weight: bold;
            }
        </style>
    </head>
    <body>
        <div id="root">
            <h1>请登录以后再做操作</h1>
            <h2 id="info"></h2>
            <form>
                <div><input id="username" type="text" /></div>
                <div><input id="password" type="password" /></div>
                <div><button id="login-btn" type="button">登录</button></div>
            </form>
        </div>

        <script>
            
            // 点击login-btn后实现登录功能
            const loginBtn = document.getElementById("login-btn")
            const root = document.getElementById("root")

            function loadData() 
            {
                // 当我们访问的是需要权限的api时,必须在请求中附加权限的信息
                // token一般都是通过请求头来发送
                const token = localStorage.getItem("token")
                fetch("http://localhost:3000/students", {
                    headers:{
                        // "Bearer xxxxxx"
                        "Authorization":`Bearer ${token}` //服务器端的setheader中需要允许
                    }
                })
                .then((res) => {
                    if (res.status === 200) {
                        return res.json() // res.json() 可以用来读取json格式的数据
                    } else { throw new Error("加载失败!") }
                })
                .then((res) => {
                    // 获取到数据后,将数据渲染到页面中
                    if (res.status === "ok") {
                        // 创建一个table
                        const dataDiv = document.getElementById("data")
                        const table = document.createElement("table")
                        dataDiv.appendChild(table)
                        table.insertAdjacentHTML("beforeend","<caption>学生列表</caption>")
                        table.insertAdjacentHTML(
                            "beforeend",
                            `
                            <thead>
                                <tr>
                                    <th>学号</th>    
                                    <th>姓名</th>    
                                    <th>年龄</th>    
                                    <th>性别</th>    
                                    <th>地址</th>    
                                </tr> 
                            </thead>
                        `)
                        
                        const tbody = document.createElement("tbody")
                        table.appendChild(tbody)
                        // 遍历数据
                        for (let stu of res.data) {
                            tbody.insertAdjacentHTML(
                                "beforeend",
                                `<tr>
                                    <td>${stu.id}</td>    
                                    <td>${stu.name}</td>    
                                    <td>${stu.age}</td>    
                                    <td>${stu.gender}</td>    
                                    <td>${stu.address}</td>    
                                </tr>`
                        )}
                    }
                })
                .catch((err) => {console.log("出错了!", err) }
            )}

            // 判断用户是否登录
            if (localStorage.getItem("token")) {
                // 用户已经登录
                root.innerHTML = `
                            <h1>欢迎 ${localStorage.getItem("nickname")} 回来!</h1>
                            <hr>
                            <button id="load-btn" onclick="loadData()">加载数据</button>
                            <button onclick="localStorage.clear()">注销</button>
                            <hr>
                            <div id="data"></div>`
            } else {
                loginBtn.onclick = () => {
                    // 获取用户输入的用户名和密码
                    const username = document.getElementById("username").value.trim()
                    const password = document.getElementById("password").value.trim()

                    // 调用fetch发送请求来完成登录
                    fetch("http://localhost:3000/login", {
                        method: "POST",
                        headers: {
                            "Content-type": "application/json"
                        },
                        body: JSON.stringify({ username, password })
                    })
                    .then((res) => res.json())
                    .then((res) => {
                        if (res.status !== "ok") { throw new Error("用户名或密码错误") }

                        // 登录成功以后,需要保持用户的登录的状态,需要将用户信息存储到本地存储
                        // 登录成功,向本地存储中插入用户的信息
                        localStorage.setItem("token", res.data.token)
                        localStorage.setItem("nickname", res.data.nickname)

                        // 登录成功
                        root.innerHTML = `
                            <h1>欢迎 ${res.data.nickname} 回来!</h1>
                            <hr>
                            <button id="load-btn" onclick="loadData()">加载数据</button>
                            <button onclick="localStorage.clear()">注销</button>
                            <hr>
                            <div id="data"></div>`
                    })
                    .catch((err) => { // 这里登录失败
                        document.getElementById("info").innerText ="用户名或密码错误"
                    })  
             }}
        </script>
    </body>
</html>

服务器端

const express = require("express")
const jwt = require("jsonwebtoken") // 引入jwt
const app = express()

const STU_ARR = [
    { id: "1", name: "孙悟空", age: 18, gender: "男", address: "花果山" },
    { id: "2", name: "猪八戒", age: 28, gender: "男", address: "高老庄" },
    { id: "3", name: "沙和尚", age: 38, gender: "男", address: "流沙河" }
]

app.use(express.urlencoded({ extended: true }))
// 解析json格式请求体的中间件
app.use(express.json())

app.use((req, res, next) => {
    // 设置响应头
    res.setHeader("Access-Control-Allow-Origin", "*")
    res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH")
    res.setHeader("Access-Control-Allow-Headers", "Content-type,Authorization")
    next()
})

// 定义一个登录的路由
app.post("/login", (req, res) => 
{
    const { username, password } = req.body
    if (username === "admin" && password === "123123") {
        // 登录成功,生成token
        const token = jwt.sign(
            {
                id: "12345",
                username: "admin",
                nickname: "超级管理员"
            },
            "chaojianquanmima",
            { expiresIn: "1d" }
        )
        res.send({
            status: "ok",
            data: {
                token,
                nickname: "超级管理员"
            }
        })
    } else { // 登录失败
        res.status(403).send({
            status: "error",
            data: "用户名或密码错误"
        })
    }
})

// 定义学生信息的路由
app.get("/students", (req, res) => {
    try {
        // 这个路由必须在用户登录后才能访问
        // req.get:读取请求头
        const token = req.get("Authorization").split(" ")[1]

        // 对token进行解码
        const decodeToken = jwt.verify(token, "chaojianquanmima")
        // 解码成功,token有效 返回学生信息
        res.send({
            status: "ok",
            data: STU_ARR
        })
    } catch (e) { // 解码错误,用户token无效
        res.status(403).send({
            status: "error",
            data: "token无效"
        })
    }
})

app.listen(3000, () => {
    console.log("服务器已经启动!")
})