AJAX

AJAX已经在Django中使用jQuery发送过了,当时原生的AJAX方法没有学。现在通过JS,多了解一下原生AJAX以及从AJAX来的跨域请求。

在2005年,Jesse James Garrett发表的一篇文章《Ajax: A New Approach to
Web Applications
》,里边给当时还没有名字的AJAX技术起了AJAX这个名字。虽然AJAX技术并不是他发明的,但从那之后,这个技术就得到了广泛的使用。很多人说如今的Web应用及前后端开发技术始于2005年,因为这一年Google推出了Google地球服务,第一次让“Web应用”深入人心,另外就是这篇文章带来了AJAX这样一个概念。

实际的AJAX技术要比这篇文章诞生的早的多。最早的时候JS通过Jave applet或FLash等中间层发送请求,后来把这种技术叫做远程脚本,再后来才有了AJAX这个名字。AJAX技术的核心是XMLHttpRequest对象(简称XHR对象),首先由微软引入。在JS中使用AJAX技术,就是操作XHR对象的一系列API。

XMLHttpRequest对象

这里有一篇阮一峰的XMLHttpRequest教程可供参考。

抛开老旧的IE6,现代浏览器都支持原生的XHR对象创建方法:

let xhr = new XMLHttpRequest()

发送AJAX请求的阶段分为如下阶段:建立请求对象—>发送请求—>收到响应—>处理响应结果。

建立请求

在使用XHR对象的时候,第一个需要调用的方法是 .open(request, URL, async?)

第一个参数是HTTP请求类型,是用字符串表示的”get”或者”post”;第二个参数是用字符串表示的请求发送到的URL,可以使用绝对或相对路径;第三个表示是否采用异步发送,true表示使用异步发送,false表示使用同步发送。

如果要同步发送一个GET请求到”/example/”,就写成:

xhr.open("get",'/exmaple/',false)

关于向哪个URL发送请求,这里有一个概念,就是同源。如果URL与发送请求的AJAX代码所在的URL不属于同源,则无法发送该请求。

发送请求

发送请求需要在已经执行过.open()方法的XHR对象上执行.send(data)方法:

参数data是发送的数据,一般可以用序列化后的JSON字符串。如果不需要发送数据,参数可以设置为null。

let xhr = new XMLHttpRequest();
xhr.open("get",'/exmaple/',false);
xhr.send("ajaxrequest");

注意,这个示例代码里采用了同步发送,一般在主进程里,不要使用同步发送。如果将URL改成比如”http://www.baidu.com”,就可以在浏览器控制台看到跨域请求被阻止了。

收到响应

代码设置为了同步响应,所以JS会一直阻塞到服务器响应。但实际上一般都发送异步响应,在实际开发中也应该发送异步的AJAX响应。在收到响应后,XHR的对象的几个属性会被响应的数据填充。

XHR收到响应后的属性
属性名 说明
responseText 作为响应主体被返回的文本
responseXML 如果响应的内容类型是”text/xml”或”application/xml”,这个属性里是XML DOM文档
status HTTP状态码
statusText HTTP状态的说明
readyState 异步发送时整个过程的阶段码,0表示未调用open方法,1表示已经调用open但未send,2表示以及send但未收到响应,3表示收到部分响应,4表示收到全部响应数据且就绪。

处理响应结果

只要readyState属性的值有变动,都会触发一次readystatechange事件。为了确保浏览器兼容性,一定要在调用.open()方法之前给XHR对象绑定readystatechange事件处理程序

从上边的属性可以看出来,发送完异步AJAX请求之后,首先是一定要监听readystatechange事件,每次触发事件都要去检测readyState属性的值。当值=4的时候,再检测HTTP的状态码。都通过检查之后,再去拿到响应文本进行处理。

典型的处理XHR响应的程序写法是:

xhr.onreadystatechange= function(){
    if ( xhr.readyState === 4 && xhr.status === 200 ) {
        //响应成功的处理
        alert( xhr.responseText );
    } else {
        //响应失败的处理
        alert( xhr.statusText );
    }
};

一般给XHR对象绑定事件,就用DOM 0级的绑定时间即可,兼容性比较好。
在异步的请求还没有接收到之前,还可以调用.abort()方法来取消异步请求,调用之后,XHR对象的事件触发会停止,所有响应属性也没有了。
无论是正常收到了请求还是终止了请求,都要将XHR对象设置成null,释放内存,不要重用XHR对象。每次使用最好都新建XHR对象。

HTTP头部信息

之前介绍完了AJAX请求发送的全流程和事件监控。如果要用AJAX发情请求,就必须知道HTTP的GET和POST方法,现在来看一看XHR对象还能够做的事情,就是对HTTP请求头部进行操作。

默认情况下,XHR由于发送的是HTTP请求,默认会发送下列头部信息:

默认头部信息
头部字段 说明
Accept 浏览器能够处理的内容类型
Accept-Charset 浏览器能够显示的字符集
Accept-Encoding 浏览器处理的压缩编码
Accept-Language 浏览器当前设置的语言
Connection 浏览器与服务器之间连接的类型
Cookie 当前页面设置的Cookie
Host 发出请求所在的域
Referer 注意,这个英文正确拼法是referrer,但HTTP1.1规范里写的就是referer,所以只能将错就错。
User-agent 用户浏览器信息

对XHR对象使用 .setRequestHeader(key,value)的方法来设置头部。实际开发中,用于向头部添加自行设置的头部字段和值,不要修改默认的HTTP请求,否则可能影响服务器的正常工作。

还有两个方法 getResponseHeader(header-key)getAllResponse()用于获得返回的响应的头部。

GET请求

通过AJAX发送GET请求需要在.open()方法中将第一个参数设置为”get”。查询字符串则需要拼接到URL内当做第二个参数内。

查询字符串的形式是 ?key1=value1&key2=value2,所有的字符串都要通过 encodeURIComponent() 方法编码之后放到URL的末尾。

这里可以用一个小函数给GET请求添加键值对:

    function addURLParam(url, name, value) {
        if (url.indexOf("?") === -1){
            url += "?"
        }
        else {
            url+="&"
        }
        let string = url + encodeURIComponent(name) + "=" + encodeURIComponent(value);
        return string
    }

上边这个函数将想要附加的查询字符串加到URL上之后,再调用.open()方法,这样才能将查询字符串发到后端,Django用 request.GET.get就可以拿到数据了。在发送GET请求的时候用.sent()方法传的数据没什么意义,后端是靠拿查询字符串获得数据的。

POST请求

POST请求与GET请求最大的区别就是,必须使用 .send() 方法来发送实际数据,而不是采取URL拼接查询字符串的方式。

这里需要注意的是POST请求的实际数据格式与查询字符串相同。但是必须修改HTTP头部 Conten-type 字段的值为 application/x-www-form-urlencoded。然后将不带?的查询字符串拼接好放到send里即可。Django里就可以用request.POST.get来取到键值对了。

XHR 2级 中的快速发送POST请求

由于通过POST请求发送表单非常常用,如果每次都去自行序列化然后修改HTTP请求头再手工发送比较麻烦,浏览器都支持使用全局对象 FormData() 生成一个表单数据对象,然后用其.append(key,value)方法一次添加一个键值对,最后直接用.send()把这个对象发送走即可,不用再设置任何HTTP请求头。

实际发送AJAX请求

学完了AJAX的基础知识以及HTTP请求的知识,已经知道了绑定事件,现在来实践操作一下原生JS发送AJAX请求。发送请求的时候需要用到上边的给URL增加查询字符串的函数addURLParam

AJAX发送GET请求

建立一个按钮,给按钮绑定点击事件用来发送AJAX请求。

<button type="button" class="btn btn-primary" id="ajax">原生发送AJAX</button>

<script>
    //给按钮绑定事件
    let xhrbutton = document.getElementById("ajax");
    xhrbutton.onclick = sendajax;

    //发送AJAX请求的函数
    function sendajax() {

        // 用let新建XHR对象,在函数执行完毕后会被销毁。不重用XHR对象。
        let xhr = new XMLHttpRequest();

        // onreadystatechange事件必须在open方法执行之前绑定
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                console.log("响应阶段是:"+xhr.readyState+' 状态码是:'+xhr.status);
                console.log("成功收到响应的内容:" + xhr.responseText);
            }
            else {
                console.log("响应阶段是:"+xhr.readyState+' 状态码是:'+xhr.status)
            }
        };

        //设置请求提交到的后端基础路径
        let url = "/ajax_test/";

        //用刚才编写的函数添加GET请求查询字符串
        url = addURLParam(url,"name","jenny");
        url = addURLParam(url,"name2","cony");

        //准备AJAX请求
        xhr.open("get", url, true);

        //发送AJAX请求
        xhr.send(null);
    }
</script>

运行之后可以看到后端成功的通过 request.GET.get()拿到了name 和name2的值。前端浏览器窗口内依次打印出1-4的请求状态码以及HTTP状态码,最后是得到的字符串。

AJAX发送POST请求

发送POST请求的方式与GET不同之处在于需要设置请求头部。如果直接将.open()的参数改成post,后端拿不到数据,因为.send()方法发送的是null,这里还需要注意的是send发送的字符串不能包含开头的”?”字符。发送POST的函数修改如下:

<script>
    let xhrbutton = document.getElementById("ajax");
    xhrbutton.onclick = sendajax;

    function sendajax() {
        let xhr = new XMLHttpRequest();

        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                console.log("响应阶段是:"+xhr.readyState+' 状态码是:'+xhr.status);
                console.log("成功收到响应的内容:" + xhr.responseText);
            }
            else {
                console.log("响应阶段是:"+xhr.readyState+' 状态码是:'+xhr.status)
            }
        };

        //无需提交URL地址,只需要提交查询字符串,所以用空字符串开始拼接
        let url = "";

        //用刚才编写的函数添加GET请求查询字符串,最后去掉开始的"?"字符
        url = addURLParam(url,"name","jenny");
        url = addURLParam(url,"name2","cony");
        url = url.slice(1);

        //第二个参数只是要接收POST请求的URL,无需拼接字符串
        xhr.open("post", "/ajax_test/", true);

        //设置请求头
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");

        //将数据字符串作为send方法的参数
        xhr.send(url);
    }
</script>

仅对修改的部分进行了注释。由此可见GET请求和POST请求的不同之处在于三个地方:

  1. .open方法的第二个参数
  2. 请求头的设置
  3. .send方法参数发送的数据

FormData()对象简化POST请求发送

FormData() 就可以省略调用外部函数拼接字符串和修改HTTP请求头两步操作,简化后的代码如下:

<script>
    let xhrbutton = document.getElementById("ajax");
    xhrbutton.onclick = sendajax;

    function sendajax() {
        let xhr = new XMLHttpRequest();

        // 建立FormData对象并且添加键值对
        let data = new FormData();
        data.append("name","jenny");
        data.append("name2","cony");

        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                console.log("响应阶段是:"+xhr.readyState+' 状态码是:'+xhr.status);
                console.log("成功收到响应的内容:" + xhr.responseText);
            }
            else {
                console.log("响应阶段是:"+xhr.readyState+' 状态码是:'+xhr.status)
            }
        };

        xhr.open("post", "/ajax_test/", true);

        //将数据字符串作为send方法的参数
        xhr.send(data);
    }
</script>

跨域资源共享

刚才介绍过了同源的概念,所谓跨域,就是用XHR朝不同源的域发请求,然后收到回复。

一开始由于浏览器的同源安全策略,为了实现跨域资源共享,开发了很多技术绕过浏览器,成为事实上的一种成熟技术,后来W3C就制订了CORS(Cross-Origin Resource Sharing)工作草案,规定了如何进行跨域资源共享。可以参考阮一峰的跨域资源共享 CORS 详解

由于CORS需要服务器和客户端同时支持,所以目前了解即可,需要用到该技术的时候再来查阅。