AJAX

615 阅读6分钟

前言:

  1. 浏览器与服务器之间的通信基础是HTTP协议
  2. 用户通过网址或表单向服务器提交请求,服务器向浏览器发送相应的响应

问题:如何通过点击能获取数据并展示到页面上?

混编模式:前端和后端一起写一个页面(前端的脚本编程方式),要用后端语言的格式。由后端完成HTML代码的构建,构建完了再返回回来一个新的页面。

如何不重新加载整个页面,却能获取到新的网页所需的数据和更新部分网页内容呢?

AJAX:Asynchronous JavaScript and XML 异步的JavaScript和XML

历史:

1999 IE5.0 允许JS脚本向服务器单独发起HTTP请求的新功能(异步,一般是浏览器通过src发起请求)

2004 Gmail 退出异步邮件更新服务

2005 Google Map 异步更新地图服务

2005 AJAX被大厂公认命名

2006 W3C发布AJAX国际标准

总结AJAX是什么?- JavaScript脚本发起HTTP通信

JavaScript异步通信:请求服务器返回JSON/XML文档,前端从XML文档中提取数据,再在不刷新整个网页的基础上,渲染到网页相应的位置。


AJAX示例

jQuery实现AJAX有下面这三种方式

        $.ajax({
            url: '...',
            type: 'POST',
            dataType: 'JSON',
            data: {
                states: 1
            },
            success: function(data){
                console.log(data);
            }
        });
        $.post('url', {status: 1}, function(){
            console.log(data);
        })
        $.get('url?status=1', function(){
            console.log(data);
        })

封装AJAX

创建XMLHTTPREQUEST对象

原生XMLHttpRequest对象与ActiveX对象

作用:

  • JS脚本HTTP请求的发起必须通过XMLHttpRequest对象

  • 通过AJAX进行浏览器与服务器通信的接口

  • 不局限于XML,可以发送任何格式的数据(XML只是以前名字的沿用)

XMLHttpRequest本身是一个Js引擎内置的构造函数

所有XMLHttpRequest对象都需要被实例化

var xhr = new XMLHttpRequest();

兼容性:IE5/IE6使用ActiveX对象

var xhr = new ActiveXObject(’Microsoft.XMLHTTP’);

Xmlhttprequest版本

XMLHttpRequest标准又分为Level 1和Level 2

XMLHttpRequest Level 1缺点:

  1. 无法发送跨域请求
  2. 不能非纯文本的数据
  3. 无法获取传输进度(大文件需要)

XMLHttpRequest Level 1I改进

  1. 可以发送跨域请求
  2. 支持获取二进制数据(非纯文本数据)
  3. 支持上传文件
  4. formData对象
  5. 可以获取传输进度
  6. 可以设置超时时间

兼容性问题

  1. IE8/9/Opara Mini不支持xhr对象 -> ActiveXObject
  2. IE10/11不支持响应类型为JSON(支持xhr对象,但是不支持xhr对象下的JSON格式)
  3. 部分浏览器不支持超时设置
  4. 部分浏览器不支持blob(文件对象的二进制数据)

xhr level2 - 五个事件

xhr.onloadstart: 绑定HTTP 请求发出的监听函数

xhr.onerror:绑定请求失败的监听函数

xhr.onload: 绑定请求成功完成的监听函数(加载完成)

xhr.onabort: 绑定请求中止(调用了abort()方法)的监听函数

xhr.onloadend: 绑定请求完成(不管成功与失败,中断了也还会响应)的监听函数

loadstart -> readyState === 4 -> load/error/abort/ -> loadend

这5个事件最好都不用,兼容性不明确


发送HTTP请求

open方法(发送设置)

参数列表

  • method:请求方式

  • url:请求发送的地址

  • async:true异步 false同步

send方法(发送请求)

参数: 发送POST请求体数据用,GET不填写

发送请求时的响应任务

onreadystatechange事件: 挂载到XMLHttpRequest对象上的事件

status状态:服务器响应的状态码(200 OK、404 未找到页面)

xhr.status/xhr.statusText:服务器回应的 HTTP 状态码/服务器发送的状态提示
状态码 状态提示
200 OK,访问正常
301 Moved Permanently,永久移动
302 Move temporarily,暂时移动
304 Not Modified,未修改
307 Temporary Redirect,暂时重定向
401 Unauthorized,未授权
403 Forbidden,禁止访问
404 Not Found,未发现指定网址
500 Internal Server Error,服务器发生错误

readyState状态:通过XMLHttpRequest对象发送HTTP请求的各阶段状态码(0-4)

当readyState变化时,将触发onreadystatechange事件执行其回调函数

状态码 状态提示
0 请求未初始化
1 服务器连接已建立
2 请求已接收
3 请求处理中
4 请求已完成,且响应已就绪

注意:readyState仅仅是针对请求的状态码,获取资源是否成功取决于status的状态

服务器响应

responseText: 获取字符串数据

responseXML: 获取XML数据

        var xhr;

        // 创建XMLHttpRequest对象
        if (window.XMLHttpRequest) {
            xhr = new XMLHttpRequest();
        }else{
            xhr = new ActiveXObject('Microsoft.XMLHTTP');
        }
        
        console.log(xhr.readyState); //0

        // 请求发送
        xhr.open('GET', 'url...', true)
        xhr.send();
        
        console.log(xhr.readyState); //1

        // 监听请求和响应的状态
        xhr.onreadystatechange = function(){
            if (xhr.readyState === 4 && states === 200) {
                console.log(xhr.readyState); //234
                // 接受数据
                console.log(JSON.parse(xhr.responseText));
            }
        }

POST请求方式的注意事项

POST请求方式下,send方法参数中的格式:a=1&b=2&c=3

xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");

POST请求方式必须设置这个请求头信息,目的是请求体中的数据转换为键值对,这样后端接收到a=1&b=2&c=3这样的数据才知道是这是一个POST方式传来的数据

直接传对象的话,消耗很大

xhr.open('POST', 'url...', true);
xhr.setRequestHeader('Content-type', 'appliaction/x-www-form-urlencoded');
xhr.send('status=1');

封装

        var $ = {
            ajax: function(opt){
                var url = opt.url;
            },
            post: function(){},
            get: function(){}
        }

用命名空间的方法,可以但是比较老套

还是用模块化比较好

var xhr = (function(){
  
  function _doAjax(opt){
    var o = window.XMLHttpRequest ? 
          new XMLHttpRequest() :
          new ActiveXObject('Microsoft.XMLHTTP');
       
    if(!o){
      throw new Error('您的浏览器不支持异步发起HTTP请求');
    }
    
    var opt = opt || {},
        type = (opt.type || 'GET').toUpperCase(),
        async = '' + opt.async === 'false' ? false : true,
        dataType = opt.dataType || 'JSON',
        jsonp = opt.jsonp || 'cb',
        jsonpCallback = opt.jsonpCallback || 'jQuery' + randomNum() + '_' + new Date().getTime();
        url = opt.url,
        data = opt.data || null,
        timeout = opt.timeout || 30000,
        error = opt.error || function(){},
        success = opt.success || function(){},
        complete = opt.complete || function(){},
        t = null;

    if(!url){
      throw new Error('您没有填写URL');
    }

    if(dataType.toUpperCase() === 'JSONP' && type !== 'GET'){
      throw new Error('如果dataType为JSONP,type请您设置GET或不设置');
    }

    if(dataType.toUpperCase() === 'JSONP'){
      var oScript = document.createElement('script');
      oScript.src = url.indexOf('?') === -1  
                        ? url + '?' + jsonp + '=' + jsonpCallback
                        : url + '&' + jsonp + '=' + jsonpCallback;
      document.body.appendChild(oScript);
      document.body.removeChild(oScript);
      window[jsonpCallback] = function(data){
        success(data);
      };
      return;
    }

    o.onreadystatechange = function(){
      if(o.readyState === 4){
        if((o.status >= 200 && o.status < 300) || o.status === 304){
          switch(dataType.toUpperCase()){
            case 'JSON':
              success(JSON.parse(o.responseText)); 
              break;
            case 'TEXT':
              success(o.responseText);
              break;
            case 'XML':
              success(o.responseXML);
              break;
            default:
              success(JSON.parse(o.responseText)); 
          } 
        }else{
          error();
        }
        complete();
        clearTimeout(t);
        t = null;
        o = null;
      }
    }

    o.open(type, url, async);
    type === 'POST' && o.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    o.send(type === 'GET' ? null : formatDatas(data));

    t = setTimeout(function(){
      o.abort();
      clearTimeout(t);
      t = null;
      o = null;
      throw new Error('本次请求已超时,API地址:' + url);
    }, timeout);
  }

  function formatDatas(obj){
    var str = '';
    for(var key in obj){
      str += key + '=' + obj[key] + '&';
    }
    return str.replace(/&$/, '');
  }

  function randomNum(){
    var num = '';
    for(var i = 0; i < 20; i++){
      num += Math.floor(Math.random() * 10);
    }
    return num;
  }

  return {
    ajax: function(opt){
      _doAjax(opt);
    },

    post: function(url, data, dataType, successCB, errorCB, completeCB){
      _doAjax({
        type: 'POST',
        url: url,
        data: data,
        dataType: dataType,
        success: csuccessCB,
        error: errorCB,
        complete: completeCB
      });
    },

    get: function(url, dataType, successCB, errorCB, completeCB){
      _doAjax({
        type: 'GET',
        url: url,
        dataType: dataType,
        success: csuccessCB,
        error: errorCB,
        complete: completeCB
      })
    }
  }
})();

请求超时

xhr.timeout: 多少毫秒后,如果请求仍然没有得到结果,就会自动终止。如果该属性等于0,就表示没有时间限制

xhr.timeout = 30000; //过了3000毫秒之后还没有响应,就会超时了,超时了就走ontimeout绑定的函数

xhr .ontimeout:绑定请求超时一个监听函数,如果发生 timeout 事件,就会执行这个监听函数

在open下面写

xhr.ontimeout = function() {
    xhr.abort(); //中断
    xhr = null; //销毁对象
}

level2兼容性不好

所以在外面用延时器模拟ontimeout就好了


异步与同步

async的值

异步(默认)(async = true):Ajax异步发送请求时,不影响页面加载、用户操作以及AJAX程序后的程序执行

同步(async = false):AJAX同步发送请求时,浏览器必须等到请求完成并响应成功后,AJAX程序后续的程序才会执行。

var datas = null;

$.ajax({
    url: ...,
    type: 'POST',
    data: {...},
    async: true,
    success: function(data){
        console.log(1);
        datas = data;
    }
});

console.log(2);
console.log(datas);

设置为异步的结果就是依次返回1,null,2(下面的2/datas和ajax的success是一起执行的,只是2更快)

设置为同步是话,结果是1,2,ajax里的data值(ajax请求会阻塞下面的代码执行)


datatype

dataType 返回的数据类型

JSON TEXT XML -> 解决如何正确返回响应格式的数据

dataType = opt.dataType || 'JSON'

xhr.responseXML 返回类型为XML

AJAX- 同时多个AJAX请求

问题:多个AJAX发送请求,只有最后请求能解决到响应?

XHR新实例的创建问题


跨域HTTP请求

为什么会有跨域的问题,就是因为同源策略导致的跨域请求资源的阻塞。

源http://test2.jsplusplus.com/向源http://test.winwin.com获取资源

1 – 服务器中转请求(常用)

同源策略是针对客户端说的,对服务器是无效的;不仅是客户端可以发起请求,服务器也可以向另一台服务器发起请求。

既然客户端发不了不同源是请求,那就发同源的请求给同源服务器(中转),然后由服务器向不同源服务器发送请求,数据返回给同源服务器,再由同源服务器返回给客户端。


2 – 设置基础域名+iframe(用的最多,不用后端写程序,前端全能搞定)

客户端增加一个iframe引入,引入一个不同源的页面,在两个页面上都设置基础域名。获取引入页面的window对象,得到引入页面的ajax函数,通过iframe引入的页面请求和它同源的数据

两个页面都要写基础域名

http://test/winwin.com/index.html

        document.domain = 'winwin.com';//返回当前文档的服务器域名

http://test2/winwin.com/index.html

        document.domain = 'winwin.com';

        var iframe = document.createElement('iframe');
        iframe.src = 'http://test/winwin.com/index.html';
        iframe.id = 'myIframe';
        iframe.style.display = 'none'; //用来跨域的,不是为了显示
        iframe.onload = function(){ //iframe引用的页面需要下载时间
            var ? = document.getElementById('myIframe').contentWindow.$; //拿到这个不同源页面window对象下面的ajax请求资源
            ?.post('http://test/winwin.com/get_courses1.php');
        }
        document.body.appendChild(iframe);

封装:

        var ajaxDomain = (function(){
            function createIframe(frameId, frameUrl) {
                var frame = document.createElement('iframe');
                frame.src = frameUrl;
                frame.id = frameId;
                frame.style.display = 'none';

                return frame;
            }

            return function(opt) {
                document.domain = opt.basicDomain;
                var frame = createIframe(opt.frameId, opt.frameUrl);
                frame.onload = function(){
                    var ? = document.getElementById(opt.frameId).contentWindow.$;
                    ?.ajax({
                        url: opt.url,
                        type: opt.type,
                        data: opt.data,
                        success: opt.success,
                        error: opt.error
                    });
                }
                document.body.appendChild(frame);
            }
        })();

http://test/winwin.com/index.html页面调用:

        ajaxDomain({
            basicDomain: 'winwin.com',
            frameUrl: 'http://test/winwin.com/index.html',
            url: 'http://test/winwin.com/get_courses1.php',
            type: 'POST',
            data: {
                status: 1
            },
            success: function(data){
                console.log(data);
            },
            error: function(){
                console.log(0);
            }
        })

3 – window.name+iframe

通过window.name可以实现互相之间传值,有共享属性

window.name的特点:

  1. 每个浏览器窗口都有一个全局变量window(包含iframe框架contentWindow)

  2. 每个window对象都有一个name属性(注意:一个窗口只有一个name属性)

  3. 该窗口被关闭前(生命周期内),所有页面共享一个name属性并有读写的权限

  4. 无论该窗口在被关闭前,载入什么页面,都不会改变name值

  5. 存储约为2M的字符串

  6. 如果父级窗口地址源和iframe的地址源不同,父级无法通过iframe.contentWindow.name获取值(同源策略干扰),但iframe内部(iframe和iframe之间)不受该规则限制

解决方案:先让iframe中的页面程序保存window.name, 然后跳转与父级窗口同源的另一个页面,父级页面可以从当前的iframe拿到该页面的window.name (不同页面可以获取同一个窗口的window.name)

将请求过来的数据放在window.name里去,iframe无论怎么跳转,都共享一份window.name,而且大家都获取的到,只有父级获取不到。

$.post('http://test/winwin.com/get_courses1.php', {
    status: 1
}, function(data){
    window.name = JSON.stringify(data);
})

iframe跳转到和test2同源的页面,那么主页面就可以获取到同源的window.name,就可以获取到上面请求到的数据。

        var flag = false;

        var iframe = document.createElement('iframe');
        
        var getDatas = function(){
            if(flag){
                var data = iframe.contentWindow.name;
                console.log(ISON.parse(data));
            }else{
                flag = true;
                setTimeout(function(){ //AJAX请求需要时间
                    iframe.contentWindow.location = 'index2.html';
                }, 500); 
            }
        }

        iframe.src = 'http://test/winwin.com/index.html';
        
        if (iframe.attachEvent) {
            iframe.attachEvent('onload', getDatas);
        }else{
            iframe.onload = getDatas;
        }

        document.body.appendChild(iframe);

这个方法一般都是传比较简单的值,传JSON数据比较少。


4 – postmessage+iframe (了解)

不常用原因:

  1. 伪造数据端漏洞

  2. xss攻击

  3. 兼容性问题

变量参数:otherWindow.postMessage(message, targetOrigin)

otherWindow: 接收方的引用

message: 要发送到接受方的数据

targetOrigin: 接收方的源,还有必须要有监听message事件

5 – hash+iframe (了解)

基本原理:利用url的hash值 #xxx来传递数据

基础工具:location.hash

6 – cors跨域

"跨域资源共享"(Cross-origin resource sharing)

任意域名:(php)

header("Access-Control-Allow-Origin: *");

单域名:

header("Access-Control-Allow-Origin: http://test2.jsplusplus.com");

多域名:

$allowed_origins = array('http://test2.jsplusplus.com', 'http://test3.jsplusplus.com');
header("Access-Control-Allow-Origin: ".$allowed_origins);

通知服务器在真正的请求中会采用哪种 HTTP 方法

header("Access-Control-Request-Methods:GET,POST");

可能会有安全问题


7 – JSONP跨域

JSONP - JSON with Padding: 跨域获取JSON数据的一种非官方的使用模式,一定是GET方式

JSONP的目的就是为了跨域拿资源,它利用script标签不受同源策略影响的机制,拿到后端脚本,让后端拼接出来前端需要的脚本(也就是函数执行),拼接好了之后,返回回来。响应好了之后就会执行本身定义好的函数。

  1. JSON和JSONP不是一个类型

    • JSON是数据交换格式

    • SONP是一种跨域获取JSON数据的交互技术(非正式的协议)

    • JSONP抓取的资源并不直接是JSON数据,而是带有JSON数据参数- 的函数执行

客户端期望返回的:{”name”:”Jacky”, “age”: ”18”}

JSONP实际返回的:callback({”name”:”Jacky”, “age”: ”18”})

同源策略到底给谁走了后门?(案例)

  1. img的src引入不同源的图片资源

  2. link的href引入不同源的样式文件资源

  3. iframe的src引入不同源的网页资源

  4. script的src引入不同源的脚本文件资源

script src引入php和引入js脚本的区别

script src引用的脚本的后缀名问题:后缀名对于script标签来说并不重要,它本身就是用来引入脚本的,只解析这个文本里的内容是什么,就算是txt格式的也可以解析执行(写的是什么,就按脚本解析什么)

get方式传值

<script type="text/javascript">
    function test1(str){
        console.log(str);
    }
    function test2(str){
        console.log(str);
    }
    function test3(str){
        console.log(str);
    }
</script>
<script src="http://test.winwin.com/jsonp.js?cb=test3"></script>

jsonp.js

function getParams(){
    var path = document.getElementById('javascript').src,
        callback = path.match(/cb=(.*)/)[1]; //替代地址上的cb值
    
    switch(callback){
        case 'test1':
            test1('test1');
            break;
        case 'test2':
            test2('test2');
            break;
        case 'test3':
            test3('test3');
            break;
        defqult:
            test1('test1');       
    }
}
getParams()

实现跨域

引入脚本,就相当于把脚本里的代码全都拿进来

<script type="text/javascript">
    function test(data){
        console.log(data);
    }
</script>
<script src="http://test.winwin.com/jsonp/jsonp.js"></script>

jsonp.js

$.ajax({
    url: 'http://test.winwin.com/get_course.php',
    type: 'POST',
    data: {
        status: 1
    },
    success: function(data){
        test(data);
    }
})

不管是js脚本,还是php脚本,反正php脚本会在后端执行完之后再返回结果,响应到前端。只要响应回来的东西是script标签认识的,就可以正常解析,有错误就按脚本的报错来。

<script type="text/javascript">
    function test(data){
        console.log(data);
    }
</script>
<script src="http://test.winwin.com/jsonp/jsonp.php?cb=test"></script>
<?php
    $cb = $_GET;
    echo $cb.'("winwin")'

cb是处理跨域返回数据的函数

php脚本返回test("winwin"),可以被js执行

但是如果有10个需要跨域的数据,就需要10个script标签?这样其实是不太合理的,所以可以动态创建脚本,获取到数据之后就删除。

var oBtn = document.createElement('btn');

oBtn.onclick = function() {
    oScript = document.createElement('script');
    document.src = "http://test.winwin.com/jsonp/jsonp.php?cb=test";//已经开始响应了
    document.body.appendChild(oScript);//执行
    document.body.removeChild(oScript);//获取到数据之后就删除
}

function test(data){
    console.log(data);
}

用jQuery JSON如何获取数据?

jQuery不用另外写处理数据的函数test,返回的数据的处理是在success里处理的。

oBtn.onclick = function() {
    $.ajax({
        url: 'http://test.winwin.com/jsonp/jsonp.php',
        type: 'get',
        dataType: 'JSONP',
        json: 'cb',
        jsonpCallback: 'test', //响应的函数名称,没有设置的话就会给一个随机的名称,其实实际上没有什么实际意义
        success: function(data) {
            console.log(data);
        }
    });
}

封装自己的AJAX JSONP

oScript = document.createElement('script');
document.src = "http://test.winwin.com/jsonp/jsonp.php?" + opt.jsonp + '=' + opt.jsonpCallback;
document.body.appendChild(oScript);
document.body.removeChild(oScript);

=>

配置项

jsonp = opt.jsonp || 'cb',
jsonpCallback = opt.jsonpCallback || 'jQuery' + randomNum() + '_' + new Date().getTime();
if(dataType.toUpperCase() === 'JSONP'){
  var oScript = document.createElement('script');
  oScript.src = url.indexOf('?') === -1  
                    ? url + '?' + jsonp + '=' + jsonpCallback
                    : url + '&' + jsonp + '=' + jsonpCallback; //存在问号就用&合并其他参数
  document.body.appendChild(oScript);
  document.body.removeChild(oScript);
  window[jsonpCallback] = function(data){
    success(data);
  };
  return;
}

未配置jsonpCallback的话,自动生成随机数

  function randomNum(){
    var num = '';
    for(var i = 0; i < 20; i++){
      num += Math.floor(Math.random() * 10);
    }
    return num;
  }

完整的部分看上面的AJAX封装


百度搜索联想词 sp0.baidu.com/5a1Fazu8AA5…

淘宝商品联想词JSONP API suggest.taobao.com/sug?code=ut…

手机号码归属地、运营商查询JSONP API www.baifubao.com/callback?cm…

当前所在城市天气JSONP API api.asilu.com/weather_v2/…

历史上的今天JSONP API api.asilu.com/today/?call…

个人身份基本信息JSONP API api.asilu.com/idcard/?cal…

有道翻译JSONP API fanyi.youdao.com/openapi.do?…

百度音乐搜索JSONP API tingapi.ting.baidu.com/v1/restserv…