网络编程 - XMLHttpRequest

348 阅读9分钟

AJAX 是异步的 JavaScript 和 XML (Asynchronous JavaScript And XML)的简称

AJAX可以使用 JSON,XML,HTML 和 text 文本等格式发送和接收数据

// 创建网络请求的AJAX对象(使用XMLHttpRequest)
const xhr = new XMLHttpRequest()

// 配置网络请求(通过open方法)
// 参数1 - 交互方式 - 大小写不区分
// 参数2 - URL请求地址
xhr.open('get', 'https://httpbin.org/get?name=Klaus&age=23')

// 监听XMLHttpRequest对象状态的变化,或者监听onload事件(请求完成时触发)
// 因为网络请求是异步的,在接收到数据后,会放入对应的宏任务队列中等待浏览器主线程调度
// 所以需要在对应的事件函数中处理对应的事件逻辑
xhr.addEventListener('readystatechange', () => {
  // 如果请求完成, 就获取响应体并在console中打印
  if (xhr.readyState === XMLHttpRequest.DONE) {
    // 返回的内容是JSON格式字符串,所以在使用前需要先使用JSON进行解析
    console.log(JSON.parse(xhr.response))
  }
})

// 发送send网络请求
xhr.send()

XHR的状态

事实上,我们在一次网络请求中看到状态发生了很多次变化,这是因为对于一次请求来说包括如下的状态

注意:

这个状态并非是HTTP的响应状态 ,而是记录的XMLHttpRequest对象的状态变化

http响应状态依旧需要通过响应行的status进行获取

image.png

const xhr = new XMLHttpRequest()

// xhr被创建的时候,xhr.readyState的默认值为0
console.log(xhr.readyState) // => 0

// xhr的配置和事件监听 没有绝对的先后顺序
xhr.addEventListener('readystatechange', () => {
  console.log(xhr.readyState)
  /*
    =>
      1
      2
      3
      4
  */

  // xhr.readyState的状态的值为 0 ~ 4
  // 直接使用数字不合适也不容易维护和阅读,所以每一个状态码 都对应XMLHttpRequest上的一个常量
  if (xhr.readyState === XMLHttpRequest.DONE) {
    console.log(xhr.response)
  }
})

xhr.open('get', 'https://httpbin.org/get?name=Klaus&age=23')

xhr.send()

同步请求

默认情况下,XHR的请求是异步的,也就是事件会单独交给网络请求线程来进行执行,拿到对应的结果后,会存放到宏任务队列中等待处理

但是我们也可以设置xhr.open方法的第三个参数,将该参数设置为false的时候就表示当前XHR请求为同步请求

该参数的默认值为true,即XHR请求的默认行为是异步请求

const xhr = new XMLHttpRequest()

xhr.open('get', 'https://httpbin.org/get?name=Klaus&age=23', false)

xhr.send()

// 因为open的第三个参数设置为了false
// 那么就意味着xhr的send方法发送的网络请求变成了同步模式
// 会阻塞send方法后边的代码的正常执行
console.log(xhr.response)

其它事件监听

事件功能
loadstart请求开始时触发
progress1. 请求正在加载中
2. 可以通过 xhr.responseText 读取当前已经接收到的部分数据
abort调用 xhr.abort() 取消了请求
error当请求遇到网络错误,如网络中断、跨域错误等情况时,会触发该事件
不会发生诸如 404 这类的 HTTP 错误
因为 HTTP 响应代码 404 不是网络错误,而是一个合法的 HTTP 响应表示资源未找到,这会导致 load 事件的触发而不是 error 事件
load当请求成功完成时触发,无论响应的 HTTP 状态码是什么,只要服务器有响应就会触发 load
timeout如果设定了超时时间,并且请求在该时间内没有完成,则触发 timeout 事件
默认超时时间为0,即不设置超时时间,直到浏览器主动断开对应的链接
loadend在请求结束时触发,不管是通过 loaderrortimeout, 或 abort 事件结束的,loadend 都会作为最后一个被触发的事件。

每个请求都从触发loadstart事件开始

接下来,通常每隔50毫秒左右触发一次progress事件

然后触发load、error、abort或timeout事件中的一个

最后以触发loadend事件结束

const xhr = new XMLHttpRequest()

xhr.open('get', 'https://httpbin.org/get?name=Klaus&age=23')

// onload 事件 会在 xhr.readyState === 4 的时候 被触发
xhr.addEventListener('load', () => console.log(xhr.response))

xhr.send()
const xhr = new XMLHttpRequest()

xhr.responseType = 'json'

// typeof e -> ProgressEvent
xhr.addEventListener('progress', e => {
  console.log(e.loaded) // => 已经接收的字节数
  console.log(e.total) // => 根据Content-Length响应头部确定的预期字节数
  console.log(e.lengthComputable) // => 一个布尔值 表示是否加载完全
  // 有了上述信息后,调用者可以自主决定如何展示对应的加载进度信息
})

xhr.open('get', 'http://www.httpbin.org/get?name=Klaus&age=23')

xhr.send()

响应数据和响应类型

发送了请求后,我们可以通过response属性来获取对应的结果

  • XMLHttpRequest response 属性返回响应的正文内容
  • 默认情况下 response属性的值是字符串的
  • 因为默认情况下responseType的值为空字符串,当responseType的值为空字符串的时候,会被解析为默认值 text
  • 所以我们可以通过修改responseType的值 来改变response对应值的类型
const xhr = new XMLHttpRequest()

xhr.open('get', 'https://httpbin.org/get?name=Klaus&age=23')

xhr.addEventListener('load', () => console.log(xhr.response))

// responseType: '' | 'text' | 'xml' | ’json‘
xhr.responseType = ''
// 上行代码等价于
// xhr.responseType = 'text'

xhr.send()
const xhr = new XMLHttpRequest()

xhr.open('get', 'http://www.example.com/text/xml')

xhr.addEventListener('load', () => {
  // response 返回的数据格式 (可以解析为普通文件 或 json)
  console.log(xhr.response)
  // 返回响应体对应的 text/plain 格式的内容
  console.log(xhr.responseText)
  // 如果返回的类型为xml,可以通过responseXML 来获取对应的解析后的xml内容
  console.log(xhr.responseXML)
})

xhr.responseType = 'xml'

xhr.send()

响应状态和状态码

XMLHttpRequest的state是用于记录xhr对象本身的状态变化,并非针对于HTTP的网络请求状态

如果我们希望获取HTTP响应的网络状态,可以通过status和statusText来获取

const xhr = new XMLHttpRequest()

xhr.open('get', 'http://www.httpbin.org/foo')

// xhr请求发送成功的时候,且响应体全部接收完毕时候执行的回调
// ps: 注意:不管状态码是多少,只要发送请求成功都会被onload方法监听
// 即使当前请求的状态码是 404 500 或 其它
xhr.addEventListener('load', () => {
  console.log(xhr.status, xhr.statusText) // 404 'NOT FOUND'
})

// xhr请求发送失败时候执行的回调
xhr.addEventListener('error', () => {
  console.log('请求发送失败')
})

xhr.responseType = 'json'

xhr.send()

参数传递

常见的参数传递方式有如下几种格式:

  1. GET请求的query参数
  2. POST请求 x-www-form-urlencoded 格式
  3. POST请求 FormData 格式
  4. POST请求 JSON 格式

GET请求的query参数

const xhr = new XMLHttpRequest()

const searchParams = new URLSearchParams()
searchParams.append('name', 'Klaus')
searchParams.append('age', 23)

// get请求在传递数据的时候,需要将参数挂载为URL的query参数
// http://www.httpbin.org/get?name=Klaus&age=23
xhr.open('get', `http://www.httpbin.org/get?${searchParams.toString()}`)

xhr.addEventListener('load', () => {
  console.log(xhr.response)
})

xhr.responseType = 'json'

xhr.send()

POST请求 x-www-form-urlencoded 格式

const xhr = new XMLHttpRequest()
xhr.open('POST', 'http://www.httpbin.org/post')

xhr.addEventListener('load', () => {
  console.log(xhr.response)
})

xhr.responseType = 'json'

// 默认情况下,post请求方式中 请求体中的数据 是按照text/plain的方式进行传输和解析的
// 所以如果传递的数据是按照x-www-form-urlencoded进行传输的时候
// 需要手动设置请求头Content-type 为 application/x-www-form-urlencoded

// 请求头名 不区分大小写 不过请求数据编码类型一遍写为 Content-type
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')

// post请求传递参数的时候
// 如果参数使用 x-www-form-urlencoded方式传递数据的时候
// 需要将对应的参数以urlencoded方式进行编码后 作为send方法的参数被传入
xhr.send(searchParams.toString())

POST请求 FormData 格式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>document</title>
</head>
<body>
  <form id="form">
    <input type="text" name="username">
    <input type="password" name="password">
  </form>
  <button id="send">send</button>

  <script>
    const btnEl = document.getElementById('send')

    btnEl.addEventListener('click', () => {
      const xhr = new XMLHttpRequest()

      const formEl = document.getElementById('form')

      xhr.open('POST', 'http://www.example.com/postform')

      xhr.addEventListener('load', () => {
        console.log(xhr.response)
      })

      xhr.responseType = 'json'

      // 如果要提交表单数据,可以使用FormData 结合表单元素 生成对应的formData实例
      const formData = new FormData(formEl)

      // 传递数据的时候 需要将对应的formData实例对象作为send方法的参数
      // 如果浏览器发送传递的数据类型为formData的时候
      // 浏览器会自动将Content-type设置为multipart/form-data
      // 不需要我们手动进行设置
      xhr.send(formData)
    })
  </script>
</body>
</html>

POST请求 JSON 格式

const xhr = new XMLHttpRequest()

xhr.open('POST', 'http://www.httpbin.org/post')

xhr.addEventListener('load', () => {
  console.log(xhr.response)
})

xhr.responseType = 'json'

// 设置Content-typed的时候可以在编码后边指定文件的类型
// 如 application/json; charset=utf-8
// 如果不指定文件类型的时候,分号必须省略,且默认的文件编码格式为utf-8
xhr.setRequestHeader('Content-type', 'application/json')

// json格式数据 传递时候 需要将json格式对象转换为json格式字符串后
// 作为send方法的参数进行传递
xhr.send(JSON.stringify({name: 'Klaus', age: 23}))

其它补充

超时时间

在网络请求的过程中,为了避免过长的时间服务器无法返回数据,通常我们会为请求设置一个超时时间: timeout

当达到超时时间后依然没有获取到数据,那么这个请求会自动被取消掉

XHR的默认超时时间为0s,表示没有设置超时时间, 一般推荐设置为10s

const xhr = new XMLHttpRequest()

xhr.responseType = 'json'

// 设置超时时间
// 当一个请求超过3s依旧没有返回值的时候
// 浏览器会自动取消该次请求,即使后续服务器返回了对应的数据,浏览器也将不再进行任何的处理
xhr.timeout = 3000

xhr.onload = () => console.log(xhr.response)

// 监听XHR的超时事件
xhr.ontimeout = () => console.log('请求超时了')

// http://www.example.com/timeout 是一个6s后才会返回对应结果的接口
xhr.open('get', 'http://www.example.com/timeout')

xhr.send()

取消请求

const xhr = new XMLHttpRequest()

xhr.responseType = 'json'

xhr.onload = () => console.log(xhr.response)

// 监听xhr的请求取消事件
// 和xhr的timeout一样 xhr的abort方法 也是在浏览器层面 取消对某一个请求的处理
// 在网络中对应的请求依旧在传输,也可能会返回对应的结果
// 只不过浏览器不会在对该请求进行任何的处理
xhr.onabort = () => console.log('请求被主动取消了')

xhr.open('get', 'http://123.207.32.32:1888/01_basic/timeout')

// 过3s后,主动取消xhr的当前请求
setTimeout(() => xhr.abort(), 3000)

xhr.send()

ajax的简单封装

// XHR方法的简单封装
// 参数比较多,所以封装为一个配置对象
function request({
  url,
  method = 'get',
  header = {},
  timeout = 10000,
  isRequestJson = true,
  data = {}
} = {}) {
  if (!url) {
    throw new Error('url is required')
  }

  const xhr = new XMLHttpRequest()

  const promise = new Promise((resolve, reject) => {
    try {
      xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(xhr.response)
        } else {
          reject({
            code: xhr.status,
            codeText: xhr.statusText
          })
        }
      })

      xhr.addEventListener('error', reject)

      // 设置返回值格式
      xhr.responseType = 'json'

      // 设置超时时间
      xhr.timeout = timeout

      // 设置响应头
      // setRequestHeader方法必须在open方法后才可以进行调用
      function setRequestHeader() {
        if(Object.keys(header).length) {
          for (const key of Object.keys(header)) {
            xhr.setRequestHeader(key, header[key])
          }
        }
      }

      if (method.toUpperCase() === 'GET') {
        // 如果请求是get请求 同时设置了data参数
        // 将其以urlencoded的方式 将对应的参数添加到url链接的最后
        const requestUrl = new URL(url)

        const searchObj = Object.fromEntries(new URLSearchParams(requestUrl.search).entries())

        // 将url字符串中的query参数和data中传入的query参数进行合并
        // 将URLSearchParams实例赋值给URL实例的search时,会自动调用其对应的toString方法
        requestUrl.search = new URLSearchParams(Object.assign(searchObj, data))

        // xhr.open的第二个参数即可以是字符串格式的url,也可以是URL的实例对象
        // 如果传入的是URL对应的实例对象,会自动调用该实例对应的toString方法
        xhr.open(method, requestUrl)
        setRequestHeader()
        xhr.send()
      } else {
        xhr.open(method, url)
        setRequestHeader()

        if (data instanceof FormData) {
          xhr.send(data)
        } else {
           xhr.setRequestHeader('Content-Type', isRequestJson ? 'application/json' : 'application/x-www-form-urlencoded')

           if (isRequestJson) {
            xhr.send(JSON.stringify(data))
           } else {
            xhr.send(new URLSearchParams(data).toString())
           }
        }

      }
    } catch (e) {
      reject(e)
    }
  })

  // 将创建出来的xhr对象挂载到返回的promise对象上
  // 以便于调用者可以拿到原生的XHR对象,并进行对应操作
  // 如执行abort方法
  promise.xhr = xhr

  return promise
}

示例 文件上传

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>XHR实现文件上传</title>
</head>
<body>
  <input type="file" id="file">
  <button id="btn">submit</button>

  <script>
    const fileEl = document.getElementById('file')
    const btnEl = document.getElementById('btn')

    btnEl.addEventListener('click', () => {
      const xhr = new XMLHttpRequest()

      xhr.open('post', 'http://www.example.com/upload')

      xhr.onload = () => console.log(xhr.response)

      // 监听上传进度
      xhr.onprogress = e => console.log(e.loaded / e.total)

      xhr.responseType = 'json'

      // 当一个input的属性为type的时候,其对应的dom元素就会多一个files属性
      // 其是一个数组,记录这所有上传的文件信息,每一个文件信息都是File对象的实例对象
      const files = fileEl.files

      // 目前前端文件上传都是依靠表单来进行完成和实现的
      // 也就是说将文件对象取出,作为表单对象的一个key-value后
      // 上传整个表单对象
      const formData = new FormData()
      formData.append('avatar', files[0])

      xhr.send(formData)
    })
  </script>
</body>
</html>