浏览器 请求 Ajax

199 阅读3分钟

Asynchronous JavaScript And XML,翻译过来就是“异步的 Javascript 和 XML”

手写 Ajax

建议先阅读MDN

创建异步对象

let xmlHttp
if (window.XMLHttpRequest) {
  // code for IE7+, Firefox, Chrome, Opera, Safari
  xmlHttp = new XMLHttpRequest()
} else {
  // code for IE6, IE5
  xmlHttp = new ActiveXObject('Microsoft.XMLHTTP')
}

创建的这个异步对象上有很多属性和方法

  1. onreadystatechange:监听异步对象请求状态码readyState的改变,每当readyState改变时,就会触发onreadystatechange事件

  2. readyState:表示异步对象目前的状态,状态码从0到4

    0: 表示请求未初始化,还没有调用 open()

    1: 服务器连接已建立,但是还没有调用 send()

    2: 请求已接收,正在处理中(通常现在可以从响应中获取内容头);

    3: 请求处理中,通常响应中已有部分数据可用了,没有全部完成;

    4: 当readyState状态码为4时,表示请求已完成;此阶段确认全部数据都已经解析完毕,可以通过异步对象的属性获取对应数据;

  3. status:http状态码

    表示成功的http状态码有

    xmlHttp.status >= 200 && xmlHttp.status < 300 || xmlHttp.status == 304

  4. responseText:后台返回的字符串形式的响应数据;

  5. responseXML:后台返回的XML形式的响应数据

设置请求方式和请求地址

  • open()方法设置ajax请求方式和请求地址

    • xmlHttp.open("GET/POST","ajax-get.txt",true)

    • 请求的类型;GET 还是 POST;

    • 表示请求的文件的地址url;

    • 设置请求方法是不是异步async,true为异步, false为同步。AJAX存在的意义就是发异步请求,所以第三个参数永远传true

  • IE中的缓存问题

    • 在IE浏览器中,如果通过Ajax发送GET请求,那么IE浏览器认为,同一个URL只有一个结果,如果地址没有发生变化,它就会把上一次返回的结果,直接返回

    • 想拿到实时数据,必须保证每次的URL都是不一样的,有两种方式

    • Math.random()

    • new Date().getTime()

    xmlHttp.open('GET', 'ajax-get.txt?t=' + new Date().getTime(), true)
    //或
    xmlHttp.open('GET', 'ajax-get.txt?t=' + Math.random(), true)
    

发送请求

xmlHttp.send()
  • 发送POST请求,使用setRequestHeader()来添加 HTTP请求头,并在send()方法中传递要发送的数据
xmlHttp.open('POST', 'ajax_test.html', true)
xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
xmlHttp.send('fname=Henry&lname=Ford')

通过onreadystatechange监听状态变化

  • 当异步对象的readyState发生改变,会触发onreadystatechange函数

  • 当readyState变成为4时,表示当前状态是请求完毕的状态

  • 同时当http的响应码status为200到300之间(包括200和300)或为304时,表示ajax请求成功

  • 当http状态码不是200到300之间的数也不是304时,表示请求不成功

//4.监听状态变化
xmlHttp.onreadystatechange = () => {
  // 判断当前状态改变是请求完毕的状态吗
  if (xmlHttp.readyState === 4) {
    if ((xmlHttp.status >= 200 && xmlHttp.status < 300) || xmlHttp.status == 304) {
      console.log('成功的接收到服务器返回的数据')
    } else {
      console.log('不成功!')
    }
  }
}

处理返回的结果

  • 如果成功,可通过异步对象的responseText属性来获取服务器返回的字符串

  • 封装一个方法ajax()用于发送请求

    1. URL当中只能出现字母 数字 下划线和ASCII码,不能出现中文,可以使用encodeURIComponent()转码

    2. 设置超时时间

ajax({
  type: 'GET',
  url: 'http://localhost:3000/posts',
  timeout: 1000,
  success: (data) => {
    console.log('success', data)
  },
  error: (err) => {
    console.log('error', err)
  },
})

其他

jQuery官方的ajax还是有一定的差异

  1. 传递多个参数,需要保持传递顺序

    • 改写成传递的是一个对象

    • 传递的是一个对象就不用考虑先后顺序,里面用的参数通过对象名.属性名的形式获取

  2. 传递请求类型的区分大小写,jQuery官方的是大小写都可以

    • 解决方案是可以使用toLowerCase()或者toUpperCase()将类型转成大写或小写再对比
  3. 我们传递的数据用的名字是obj,jQuery官方用的是data

const ajax = (option) => {
  //type, url, data, timeout, success, error将所有参数换成一个对象{}

  //  0.将对象转换成字符串

  //处理obj
  const objToString = (data) => {
    data.t = new Date().getTime()
    let res = []
    for (let key in data) {
      //需要将key和value转成非中文的形式,因为url不能有中文。使用encodeURIComponent();
      res.push(encodeURIComponent(key) + ' = ' + encodeURIComponent(data[key]))
    }
    return res.join('&')
  }

  let str = objToString(option.data || {})

  //  1.创建一个异步对象xmlHttp;
  var xmlHttp, timer
  if (window.XMLHttpRequest) {
    xmlHttp = new XMLHttpRequest()
  } else if (xmlHttp) {
    // code for IE6, IE5
    xmlHttp = new ActiveXObject('Microsoft.xmlHttp')
  }

  //  2.设置请求方式和请求地址;
  // 判断请求的类型是POST还是GET
  if (option.type.toLowerCase() === 'get') {
    xmlHttp.open(option.type, option.url + '?t=' + str, true)
    //  3.发送请求;
    xmlHttp.send()
  } else {
    xmlHttp.open(option.type, option.url, true)
    // 注意:在post请求中,必须在open和send之间添加HTTP请求头:setRequestHeader(header,value);
    xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
    //  3.发送请求;
    xmlHttp.send(str)
  }

  //  4.监听状态的变化;
  xmlHttp.onreadystatechange = function () {
    clearInterval(timer)
    debugger
    if (xmlHttp.readyState === 4) {
      if ((xmlHttp.status >= 200 && xmlHttp.status < 300) || xmlHttp.status == 304) {
        //  5.处理返回的结果;
        option.success(xmlHttp.responseText) //成功后回调;
      } else {
        option.error(xmlHttp.responseText) //失败后回调;
      }
    }
  }

  //判断外界是否传入了超时时间
  if (option.timeout) {
    timer = setInterval(function () {
      xmlHttp.abort() //中断请求
      clearInterval(timer)
    }, option.timeout)
  }
}

测试

  • mock数据
{
  "posts": [
    {
      "id": 1,
      "title": "json-server",
      "author": "xianzao"
    }
  ],
  "comments": [
    {
      "id": 1,
      "body": "some comment",
      "postId": 1
    }
  ],
  "profile": {
    "name": "xianzao"
  }
}
  • 执行
ajax({
  type: 'GET',
  url: 'http://localhost:3000/posts',
  timeout: 1000,
  success: (data) => {
    console.log('success', data)
  },
  error: (err) => {
    console.log('error', err)
  },
})

针对fusion中实现Upload的request封装

function customRequest(option) {
    /* coding here */
    return {abort() {/* coding here */}};
}

<Upload request={customRequest}/>
  • customRequest的实现
/**
 * clone from https://github.com/react-component/upload/blob/master/src/request.js
 */

function getError(option, xhr, msg) {
  msg = msg || `cannot post ${option.action} ${xhr.status}'`
  const err = new Error(msg)
  err.status = xhr.status
  err.method = option.method
  err.url = option.action
  return err
}

function getBody(xhr) {
  const text = xhr.responseText || xhr.response
  if (!text) {
    return text
  }

  try {
    return JSON.parse(text)
  } catch (e) {
    return text
  }
}

// option {
//  onProgress: (event: { percent: number }): void,
//  onError: (event: Error, body?: Object): void,
//  onSuccess: (body: Object): void,
//  data: Object,
//  filename: String,
//  file: File,
//  withCredentials: Boolean,
//  action: String,
//  headers: Object,
//  method: String
//  timeout: Number
// }
export default function upload(option) {
  const xhr = new XMLHttpRequest()

  if (option.onProgress && xhr.upload) {
    xhr.upload.onprogress = function progress(e) {
      if (e.total > 0) {
        e.percent = (e.loaded / e.total) * 100
      }
      option.onProgress(e)
    }
  }

  const formData = new FormData()

  if (option.data) {
    Object.keys(option.data).forEach((key) => {
      formData.append(key, option.data[key])
    })
  }
  if (option.file instanceof Blob) {
    formData.append(option.filename, option.file, option.file.name)
  } else {
    formData.append(option.filename, option.file)
  }

  xhr.onerror = function error(e) {
    option.onError(e)
  }

  xhr.onload = function onload() {
    // allow success when 2xx status
    // see https://github.com/react-component/upload/issues/34
    if (xhr.status < 200 || xhr.status >= 300) {
      return option.onError(getError(option, xhr), getBody(xhr))
    }

    option.onSuccess(getBody(xhr), xhr)
  }

  option.method = option.method || 'POST'
  xhr.open(option.method, option.action, true)

  // In Internet Explorer, the timeout property may be set only after calling the open() method and before calling the send() method.
  // see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout
  const { timeout } = option

  if (typeof timeout === 'number' && timeout > 0) {
    xhr.timeout = timeout
    xhr.ontimeout = () => {
      const msg = `Upload abort for exceeding time (timeout: ${timeout}ms)`
      option.onError(getError(option, xhr, msg), getBody(xhr))
    }
  }

  // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
  if (option.withCredentials && 'withCredentials' in xhr) {
    xhr.withCredentials = true
  }

  const headers = option.headers || {}

  // when set headers['X-Requested-With'] = null , can close default XHR header
  // see https://github.com/react-component/upload/issues/33
  if (headers['X-Requested-With'] !== null) {
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
  }

  for (const h in headers) {
    if (headers.hasOwnProperty(h) && headers[h] !== null) {
      xhr.setRequestHeader(h, headers[h])
    }
  }
  xhr.send(formData)

  return {
    abort() {
      xhr.abort()
    },
  }
}