axios 源碼深入分析之 XHR 和 axios 的理解和使用

一、XHR 的相關內容

  1. MDN 文檔 :https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
  2. 理解 XHR,如下所示:
  • 使用 XMLHttpRequest (XHR) 對象可以與服務器交互, 也就是發送 ajax 請求
  • 前端可以獲取到數據,而無需讓整個的頁面刷新。
  • 這使得 Web 頁面可以只更新頁面的局部,而不影響用戶的操作。
  1. 區別 ajax 請求與一般 HTTP 請求,如下所示:
  • ajax 請求是一種特別的 http 請求:
  • 只有通過 XHR/fetch 發送的是 ajax 請求, 其它都是一般 HTTP 請求
  • 對服務器端來說, 沒有任何區別, 區別在瀏覽器端
    瀏覽器端發請求:
  • 只有 XHRfetch 發出的纔是 ajax 請求, 其它所有的都是非 ajax 請求
  • 瀏覽器端接收到響應,如下:
    • 一般請求: 瀏覽器一般會直接顯示響應體數據, 也就是我們常說的刷新/跳轉頁面
    • ajax 請求: 瀏覽器不會對界面進行任何更新操作, 只是調用監視的回調函數並傳入響應相關數據
  1. API,如下所示:
  • XMLHttpRequest(): 創建 XHR 對象的構造函數
  • status: 響應狀態碼值, 比如 200, 404
  • statusText: 響應狀態文本
  • readyState: 標識請求狀態的只讀屬性
    • 0: 初始
    • 1: open()之後
    • 2: send()之後
    • 3: 請求中
    • 4: 請求完成
  • onreadystatechange: 綁定 readyState 改變的監聽
  • responseType: 指定響應數據類型, 如果是 'json' , 得到響應後自動解析響應體數據
  • response: 響應體數據, 類型取決於 responseType 的指定
  • timeout: 指定請求超時時間, 默認爲 0 代表沒有限制
  • ontimeout: 綁定超時的監聽
  • onerror: 綁定請求網絡錯誤的監聽
  • open(): 初始化一個請求, 參數爲: (method, url[, async])
  • send(data): 發送請求
  • abort(): 中斷請求
  • getResponseHeader(name): 獲取指定名稱的響應頭值
  • getAllResponseHeaders(): 獲取所有響應頭組成的字符串
  • setRequestHeader(name, value): 設置請求頭
  1. 使用 XHR 封裝一個發 ajax 請求的通用函數,如下所示:
  • 函數的返回值爲 promise, 成功的結果爲 response, 異常的結果爲 error
  • 能處理多種類型的請求: GET/POST/PUT/DELETE
  • 函數的參數爲一個配置對象,url/method/params/data,如下:
    { 
      url: '', // 請求地址 
      method: '', // 請求方式 GET/POST/PUT/DELETE 
      params: {}, // GET/DELETE 請求的 query 參數 
      data: {}, // POST 或 DELETE 請求的請求體參數
    }
    
  • 響應 json 數據自動解析爲了 js
  1. 使用 XHR 封裝一個發 ajax 請求的通用函數,代碼如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>使用XHR封裝ajax請求參數</title>
</head>
<body>
  <button onclick="testGet()">發送GET請求</button><br>
  <button onclick="testPost()">發送POST請求</button><br>
  <button onclick="testPut()">發送PUT請求</button><br>
  <button onclick="testDelete()">發送Delete請求</button><br>


  <script>

    /* 1. GET請求: 從服務器端獲取數據*/
    function testGet() {
      axios({
        // url: 'http://localhost:3000/posts',
        url: 'http://localhost:3000/posts2',
        method: 'GET',
        params: {
          id: 1,
          xxx: 'abc'
        }
      }).then(
        response => {
          console.log(response)
        },
        error => {
          alert(error.message)
        }
      )
    }

    /* 2. POST請求: 服務器端保存數據*/
    function testPost() {
      axios({
        url: 'http://localhost:3000/posts',
        method: 'POST',
        data: {
          "title": "json-server---", 
          "author": "typicode---"
        }
      }).then(
        response => {
          console.log(response)
        },
        error => {
          alert(error.message)
        }
      )
    }

    /* 3. PUT請求: 服務器端更新數據*/
    function testPut() {
      axios({
        url: 'http://localhost:3000/posts/1',
        method: 'put',
        data: {
          "title": "json-server+++", 
          "author": "typicode+++"
        }
      }).then(
        response => {
          console.log(response)
        },
        error => {
          alert(error.message)
        }
      )
    }

    /* 2. DELETE請求: 服務器端刪除數據*/
    function testDelete() {
      axios({
        url: 'http://localhost:3000/posts/2',
        method: 'delete'
      }).then(
        response => {
          console.log(response)
        },
        error => {
          alert(error.message)
        }
      )
    }

    function axios({
      url,
      method='GET',
      params={},
      data={}
    }) {
      // 返回一個promise對象
      return new Promise((resolve, reject) => {

        // 處理method(轉大寫)
        method = method.toUpperCase()

        // 處理query參數(拼接到url上)   id=1&xxx=abc
        let queryString = ''
        Object.keys(params).forEach(key => {
          queryString += `${key}=${params[key]}&`
        })
        if (queryString) { // id=1&xxx=abc&
          // 去除最後的&
          queryString = queryString.substring(0, queryString.length-1)
          // 接到url
          url += '?' + queryString
        }


        // 1. 執行異步ajax請求
        // 創建xhr對象
        const request = new XMLHttpRequest()
        // 打開連接(初始化請求, 沒有請求)
        request.open(method, url, true)

        // 發送請求
        if (method==='GET' || method==='DELETE') {
          request.send()
        } else if (method==='POST' || method==='PUT'){
          request.setRequestHeader('Content-Type', 'application/json;charset=utf-8') // 告訴服務器請求體的格式是json
          request.send(JSON.stringify(data)) // 發送json格式請求體參數
        }

        // 綁定狀態改變的監聽
        request.onreadystatechange = function () {
          // 如果請求沒有完成, 直接結束
          if (request.readyState!==4) {
            return
          }
          // 如果響應狀態碼在[200, 300)之間代表成功, 否則失敗
          const {status, statusText} = request
          // 2.1. 如果請求成功了, 調用resolve()
          if (status>=200 && status<=299) {
            // 準備結果數據對象response
            const response = {
              data: JSON.parse(request.response),
              status,
              statusText
            }
            resolve(response)
          } else { // 2.2. 如果請求失敗了, 調用reject()
            reject(new Error('request error status is ' + status))
          }
        }
      })
    }

  </script>
</body>
</html>

二、axios 的理解和使用

  1. axios 是什麼,如下所示:
  • 前端最流行的 ajax 請求庫
  • react/vue 官方都推薦使用 axiosajax 請求
  • 文檔: https://github.com/axios/axios
  1. axios 的特點,如下所示:
  • 基於 promise 的封裝 XHR 的異步 ajax 請求庫
  • 瀏覽器端/ node 端都可以使用
  • 支持請求/響應攔截器
  • 支持請求取消
  • 請求/響應數據轉換
  • 批量發送多個請求
  1. axios 常用語法,如下所示:
  • axios(config): 通用/最本質的發任意類型請求的方式
  • axios(url[, config]): 可以只指定 urlget 請求
  • axios.request(config): 等同於 axios(config)
  • axios.get(url[, config]): 發 get 請求
  • axios.delete(url[, config]): 發 delete 請求
  • axios.post(url[, data, config]): 發 post 請求
  • axios.put(url[, data, config]): 發 put請求
  • axios.defaults.xxx: 請求的默認全局配置
  • axios.interceptors.request.use(): 添加請求攔截器
  • axios.interceptors.response.use(): 添加響應攔截器
  • axios.create([config]): 創建一個新的 axios (它沒有下面的功能)
  • axios.Cancel(): 用於創建取消請求的錯誤對象
  • axios.CancelToken(): 用於創建取消請求的 token 對象
  • axios.isCancel(): 是否是一個取消請求的錯誤
  • axios.all(promises): 用於批量執行多個異步請求
  • axios.spread(): 用來指定接收所有成功數據的回調函數的方法
  1. axios 的基本使用,代碼如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>axios基本使用</title>
</head>
<body>
  
    <div>
      <button onclick="testGet()">GET請求</button>
      <button onclick="testPost()">POST請求</button>
      <button onclick="testPut()">PUT請求</button>
      <button onclick="testDelete()">DELETE請求</button>
    </div>
  
    <script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
    <script>
      // 指定默認配置
      axios.defaults.baseURL = 'http://localhost:3000'

      /* 1. GET請求: 從服務器端獲取數據*/
      function testGet() {
        // axios.get('/posts?id=1')
        axios({
          url: '/posts',
          params: {
            id: 1
          }
        })
          .then(response => {
            console.log('/posts get', response.data)
          })
      }
  
      /* 2. POST請求: 向服務器端添加新數據*/
      function testPost() {
        // axios.post('/posts', {"title": "json-server3", "author": "typicode3"})
        axios({
          url: '/posts',
          method: 'post',
          data: {"title": "json-server4", "author": "typicode4"}
        })
          .then(response => {
            console.log('/posts post', response.data)
          })
      }
  
      /* 3. PUT請求: 更新服務器端已經數據 */
      function testPut() {
        // axios.put('http://localhost:3000/posts/4', {"title": "json-server...", "author": "typicode..."})
        axios({
          url: '/posts/4',
          method: 'put',
          data: {"title": "json-server5", "author": "typicode5"}
        })
          .then(response => {
            console.log('/posts put', response.data)
          })
      }
  
      /* 4. DELETE請求: 刪除服務器端數據 */
      function testDelete() {
        // axios.delete('http://localhost:3000/posts/4')
        axios({
          url: '/posts/5',
          method: 'delete'
        })
          .then(response => {
            console.log('/posts delete', response.data)
          })
      }
  
    </script>
</body>
</html>
  1. axios.create(config),如下:
  • 根據指定配置創建一個新的 axios, 也就就每個新 axios 都有自己的配置
  • axios 只是沒有取消請求和批量發請求的方法, 其它所有語法都是一致的
  • 爲什麼要設計這個語法? 如下所示:
    • 需求: 項目中有部分接口需要的配置與另一部分接口需要的配置不太一樣, 如何處理
    • 解決: 創建兩個新 axios, 每個都有自己特有的配置, 分別應用到不同要 求的接口請求中
  1. axios.create(),代碼如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>axios.create()</title>
</head>
<body>
  <script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
  <script>
    axios.defaults.baseURL = 'http://localhost:3000'

    // 使用axios發請求
    axios({
      url: '/posts' // 請求3000
    })

    // axios({
    //   url: '/xxx' // 請求4000
    // })

    const instance = axios.create({
      baseURL: 'http://localhost:4000'
    })

    // 使用instance發請求
    // instance({
    //   url: '/xxx'  // 請求4000
    // })
    instance.get('/xxx')
  </script>
</body>
</html>
  1. 攔截器函數/ ajax 請求/請求的回調函數的調用順序,如下所示:
  • 說明: 調用 axios() 並不是立即發送 ajax 請求, 而是需要經歷一個較長的流程
  • 流程: 請求攔截器2 => 請求攔截器 1 => 發ajax請求 => 響應攔截器1 => 響 應攔截器 2 => 請求的回調
  • 注意: 此流程是通過 promise 串連起來的, 請求攔截器傳遞的是 config, 響應 攔截器傳遞的是 response
  1. axios 的處理鏈流程,代碼如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>axios的處理鏈流程</title>
</head>
<body>
  
  <script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
  <script>

    // 添加請求攔截器(回調函數)
    axios.interceptors.request.use(
      config => {
        console.log('request interceptor1 onResolved()') // 2 後
        return config
      },
      error => {
        console.log('request interceptor1 onRejected()')
        return Promise.reject(error);
      }
    )
    axios.interceptors.request.use(
      config => {
        console.log('request interceptor2 onResolved()') // 1 先
        return config
      },
      error => {
        console.log('request interceptor2 onRejected()')
        return Promise.reject(error);
      }
    )
    // 添加響應攔截器
    axios.interceptors.response.use(
      response => {
        console.log('response interceptor1 onResolved()') // 3 先
        return response
      },
      function (error) {
        console.log('response interceptor1 onRejected()')
        return Promise.reject(error);
      }
    )
    axios.interceptors.response.use(
      response => {
        console.log('response interceptor2 onResolved()') // 4 後
        return response
      },
      function (error) {
        console.log('response interceptor2 onRejected()')
        return Promise.reject(error);
      }
    )

    axios.get('http://localhost:3000/posts')
      .then(response => {
        console.log('data', response.data)
      })
      .catch(error => {
        console.log('error', error.message)
      })

    /* axios.get('http://localhost:/posts2')
      .then(response => {
        console.log('data', response.data)
      })
      .catch(error => {
        console.log('error', error.message)
      }) 
    */
  </script>
</body>
</html>

最後的處理結果,request interceptor2 是第一個請求,request interceptor1 是第二個請求,response interceptor1 是第一個響應,response interceptor2 是第二個響應。

  1. 取消請求,如下所示:
  • 基本流程,如下:
    • 配置 cancelToken 對象
    • 緩存用於取消請求的 cancel 函數
    • 在後面特定時機調用 cancel 函數取消請求
    • 在錯誤回調中判斷如果 errorcancel, 做相應處理
  • 實現功能,如下:
    • 點擊按鈕, 取消某個正在請求中的請求 在請求一個接口前,
    • 取消前面一個未完成的請求
  1. 對於 axios 取消請求的,代碼如下所示:
  • 第一個:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>取消請求</title>
    </head>
    <body>
      <button onclick="getProducts1()">獲取商品列表1</button><br>
      <button onclick="getProducts2()">獲取商品列表2</button><br>
      <button onclick="cancelReq()">取消請求</button><br>
    
      <script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
      <script>
        let cancel  // 用於保存取消請求的函數
        function getProducts1() {
          axios({
            url: 'http://localhost:4000/products1',
            cancelToken: new axios.CancelToken((c) => { // c是用於取消當前請求的函數
              // 保存取消函數, 用於之後可能需要取消當前請求
              cancel = c
            })
          }).then(
            response => {
              cancel = null
              console.log('請求1成功了', response.data)
            },
            error => {
              cancel = null
              console.log('請求1失敗了', error.message, error)
            }
          )
        }
    
        function getProducts2() {
          axios({
            url: 'http://localhost:4000/products2'
          }).then(
            response => {
              console.log('請求2成功了', response.data)
            },
            error => {
              cancel = null
              console.log('請求2失敗了', error.message)
            }
          )
        }
    
        function cancelReq() {
          // alert('取消請求')
          // 執行取消請求的函數
          if (typeof cancel === 'function') {
            cancel('強制取消請求')
          } else {
            console.log('沒有可取消的請求')
          }
        }
      </script>
    </body>
    </html>
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章