likes
comments
collection

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法

作者站长头像
站长
· 阅读数 19

一、文章简介

 现在浏览器向服务器发起异步HTTP请求获取数据的库、方法有很多,比如大家所熟知的jQuery的AJAX以及ES6中的fetch、网络请求库axios、react中常用到的useRequest等。但可能大家都没有怎么使用过JS原生的网络请求XMLHttpRequest,本文就以ES5的语法根据原生XMLHttpRequest简单封装一个AJAX,后文简称为原生AJAX


二、基本环境搭建

 这里我们使用nodejs+express快速搭建后端服务:

  1. 创建server文件夹
  2. 进入文件夹执行命令npm init -y(-y为所有选项选择默认设置)
  3. 使用命令npm install express
  4. 编写app.js,配置服务器相关内容
// 引入express依赖
const express = require('express');
// 获取express实例
const app = express();

// 设置app的路由信息
// 接收到'/'的路径请求时, 返回success结果
app.get('/', (req, res) => {
  res.statusCode = 200;
  res.send('success');
});

// 服务器于本地3000端口开启
app.listen(3000, () => {
  console.log('server is running at port 3000.');
});
  1. 配置package.json

如果没有安装过nodemon,可以先执行命令npm i -g nodemon

{
  "name": "server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",  
  "scripts": {    
    "dev": "nodemon ./app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}
  1. 执行命令npm run dev或直接执行命令node ./app.js(注意要处于server目录下)

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法


三、原生AJAX初体验

在这里我们简单创建一个原生AJAX用以获取服务器数据。

var xhr = new XMLHttpRequest();
// 设置xhr状态改变事件监听函数
xhr.onreadystatechange = function () {
  // 当服务器返回的状态为200且xhr对象的状态处于"请求已完成,响应已就绪状态"
  // 具体的readyState内容后续再谈
  if (xhr.status === 200 && xhr.readyState === 4) {
    // 打印服务器返回结果
    console.log(xhr.responseText);
  }
}
// xhr对象向地址http://localhost:3000/建立HTTP连接
xhr.open('GET', 'http://localhost:3000/', true);
// xhr对象向服务器发送GET异步请求
xhr.send();

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法

 此时我们得到结果是请求跨域被跨域策略拦截,那么我们简单聊聊跨域:跨域就是客户端的协议+域名+端口与服务器的不相同,如localhost:3000与localhost:6693因为端口不相同,因此被浏览器跨域资源共享协议所拦截。对于跨域我们会有8种解决方案,本次因为重点不在此,使用CORS的方案解决跨域。  我们在app.js中添加上允许跨域的响应头:

app.get('/', (req, res) => {
  // ⭐添加允许访问的访问源为: '任意'
  res.setHeader('Access-Control-Allow-Origin', '*');
  // ⭐添加允许访问的请求方式为: 'GET'
  res.setHeader('Access-Control-ALlow-Method', 'get');
  res.statusCode = 200;
  res.send('success');
});

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法

 我们成功解决了跨域问题,得到了服务端返回的数据success,以上就是xhr对象的初体验。


四、简单聊聊xhr的readyState

 readyState是xhr对象发送HTTP请求各阶段的状态码,一共有五种状态分别对应状态码0-4,而readyState是不能反应服务器是否正确返回信息给客户端的,需要根据xhr对象上另一个属性status来进行判断请求是否完成。

  • 0:请求未初始化
  • 1:连接已经成功建立
  • 2:服务器接收请求
  • 3:服务器处理中
  • 4:请求已完成,响应已就绪
 // 当xhr对象实例化后, readyState处于状态 0
 var xhr = new XMLHttpRequest();
 console.log('readyState at: ', xhr.readyState);
 xhr.onreadystatechange = function () {      
   // 状态2、3、4都在事件处理函数中体现
   console.log('readyState at: ', xhr.readyState);
   // 因此status = 200、readyState = 4 意为xhr对象成功获取到服务器返回数据
   if (xhr.status === 200 && xhr.readyState === 4) {        
     console.log(xhr.responseText);
   }
 }
 // 当xhr调用open方法后, readyState处于状态 1
 xhr.open('GET', 'http://localhost:3000/', true);    
 xhr.send();

五、初步封装XMLHttpRequest Level1标准

1. 定义工具函数utils/ajax.js

 我们使用自执行函数将私有方法封装在我们的ajax库内部,向外暴露一个对象接口,返回三个工具方法ajax、get、post,大家可以根据自己的需要添加更多方法。

var $ = (function () {


  return {
    ajax: function(options) {

    },
    get: function(url, successCB, errorCB) {
      
    },
    post: function(url, data, successCB, errorCB) {
      
    }
  }
}());

2. 利用关键函数_ajax来统一处理多种方式请求

我们的关键逻辑其实是在_ajax方法中实现的,在抛出给外界的方法中也只是内部调用了关键方法来进行AJAX请求。简单介绍一下 关键参数。url:请求地址、method:请求方式、successCB:请求成功执行的回调函数、errorCB:请求失败执行的回调函数、data:请求体

var $ = (function () {

  function _ajax(options) {

  }

  return {
    ajax: function(options) {
      _ajax(options);
    },
    get: function(url, successCB, errorCB) {
      _ajax({
        url: url,
        method: 'GET',
        successCB: successCB,
        errorCB: errorCB
      });
    },
    post: function(url, data, successCB, errorCB) {
      _ajax({
        url: url,
        method: 'POST',
        data: data,
        successCB: successCB,
        errorCB: errorCB
      });
    }
  }
}());

3. 封装关键函数_ajax

 function _ajax(opt) {
   // 兼容性创建xhr对象
   // 其中IE5/6是不支持XMLHttpRequest的
   // IE5/6/Opera Mini是只支持微软的ActiveXObject对象发送HTTP请求
   var xhr = window.XMLHttpRequest 
     ? new XMLHttpRequest() 
     : new ActiveXObject('Microsoft.XMLHTTP');

   if (!xhr) {
     // 如果无法创建xhr对象, 返回异常信息
     throw new Error('您的浏览器不支持异步发送HTTP请求');
   }

   // 从opt对象中获取AJAX配置信息
   var opt = opt || {},
     url = opt.url,
     method = (opt.method || 'GET').toUpperCase(),
     data = opt.data || null,
     // 请求成功的回调函数
     successCB = opt.successCB || function() {},
     // 无论请求是否成功都会执行的完成回调函数类似于finally
     completeCB = opt.completeCB || function() {},
     // 请求失败的回调函数
     errorCB = opt.errorCB || function() {},
     // 请求是否异步
     // 如果直接使用opt.async || true,传入的值为false时也会因为或运算符而得到true值
     async = '' + opt.async === 'false' ? false : true

   // 设置xhr对象的状态改变事件监听处理函数
   xhr.onreadystatechange = function () {
     if (xhr.readyState === 4) {
       // 200-300之间都为请求成功的状态码, 304为浏览器使用缓存的重定向状态码
       if ((xhr.status >= 200 && xhr.status <= 300) || xhr.status === 304) {          
         // 请求成功执行成功回调函数
         successCB(xhr.responseText);
       } else {
         // 请求失败执行失败回调函数
         errorCB(xhr.responseText);          
       }
       // 请求完成执行完成回调函数
       completeCB(xhr.responseText);
     } 
   }

   // 建立客户端与服务器的连接、设置相应url、方法以及是否异步
   xhr.open(method, url, async);

   // 当请求为POST请求时要设置客户端的Content-type以使服务器识别到请求是post请求的键值对形式
   // 此处也可以作为配置对象中的一个属性 -> 成为一个配置项
   method === 'POST' && xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

   // xhr对象发送请求,如果是GET请求不传入请求体、是POST请求则传入标准格式请求体
   // 如 name=zhangsan&&age=13
   xhr.send(method === 'GET' ? null : stringify(data));
 }

 // 将对象转化为标准请求体格式
 function stringify(data) {
   var str = '';
   for (var key in data) {
     if (Object.prototype.hasOwnProperty.call(data, key)) {
       str += key + '=' + data[key] + '&';
     }
   }
   return str.replace(/&$/, '');
 }

六、_AJAX小试牛刀

 在客户端页面用我们封装好的$_AJAX向我们搭建的服务器发送一条数据请求

  <script type='text/javascript' src='./utils/ajax.js'></script>
  <script type='text/javascript'>
    $.get('http://localhost:3000', function (result) {
      console.log(result);
    })
  </script>

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法  我们也成功获取到的服务器返回的结果。


七、XMLHttpRequest Level2中新增的超时时间

 对于Level2中的超时时间,xhr上有一个属性timeout以及超时事件句柄ontimeout可以分别设置超时时间以及超时后的事件处理函数。但是, Level2版本新增的超时时间相关api具有一定的兼容性问题。

1. xhr timeout和ontimeout

在封装的AJAX中添加超时处理逻辑, 注意要一切xhr的逻辑处理都要在send函数执行之前完成

    // Level2中的超时api
    xhr.timeout = 500;
    xhr.ontimeout = function() {
      console.log('ajax timeout');
    }

 接着我们将浏览器的网速调成Slow 3G, 重新发送AJAX请求:

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法

2. 兼容性更好的自定义timeout

 通过Level2提供的Api我们能够完成对请求超时的控制,但先前也说了这种方式具有一定的兼容性问题,我们完全可以利用setTimeout手动做一个超时监听。

// xhr对象发送请求,如果是GET请求不传入请求体、是POST请求则传入标准格式请求体
// 如 name=zhangsan&&age=13
xhr.send(method === 'GET' ? null : stringify(data));

// 兼容性timeout
// 发送请求后设置超时定时器,若在设定的超时时间内未清除定时器,则判断请求超时
t = setTimeout(function() {
  timeoutCB();
  xhr.abort();
  // 以下是一种写法,可以省略
  clearTimeout(t);
  xhr = null;
  t = null;
  throw new Error('请求超时');
}, timeout);

// 同时在xhr对象状态变化的事件处理函数中对超时进行处理
 // 设置xhr对象的状态改变事件监听处理函数
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    // 当readyState处于状态4时,表示请求已完成、响应已就绪即未超时
    // 我们取消定时任务
    clearTimeout(t);        
    // 200-300之间都为请求成功的状态码, 304为浏览器使用缓存的重定向状态码
    if ((xhr.status >= 200 && xhr.status <= 300) || xhr.status === 304) {          
      // 请求成功执行成功回调函数
      successCB(xhr.responseText);
    } else {
      // 请求失败执行失败回调函数
      errorCB(xhr.responseText);          
    }
    // 请求完成执行完成回调函数
    completeCB(xhr.responseText);
    t = null;
    xhr = null;
  } 
}

 我们再发送一条AJAX请求、利用浏览器的慢网速进行测试:

 <script type='text/javascript' src='./utils/ajax.js'></script>
 <script type='text/javascript'>
   $.ajax({
     method: 'GET',
     url: 'http://localhost:3000/',
     successCB: function(res) {
       console.log(res)
     },
     timeout: 300,
     timeoutCB: function () {
       console.log('ajax timeout');
     }
   });
 </script>

非常简单的原生AJAX的封装+请求案例+xhr level2中的超时控制及兼容性写法

 可以看到,同样达成了我们想要的超时控制效果。

八、小结一下

 AJAX的内容还是相当之多的,还会遇上跨域的问题以及如何解决跨域、各种状态码的判读、强制缓存与协商缓存的区别以及如何设置、文件上传等等。还需要我慢慢去啃去学!

 上文便是本篇文章的全部内容,因为是纯业余的技术文章分享,没有过多去检查文章是否存在错误、语义不清等问题,请大伙儿见谅!如果有什么内容问题或不理解的地方欢迎大家在评论区或私信笔者一起讨论!

 我是Donp1,一个前端菜鸡,我们下次再见~