讲师的博客地址:http://www.cnblogs.com/wupeiqi/articles/5703697.html 。号称是AJAX全套

原生Ajax

Ajax主要就是使用 XmlHttpRequest 对象来完成请求的操作,该对象在主流浏览器中均存在(除了早期的IE)。创建 XMLHttpRequest 对象的语法:

xmlhttp=new XMLHttpRequest();

老版本的 Internet Explorer (IE5 和 IE6)使用 ActiveX 对象:

xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");

XmlHttpRequest对象的主要方法

创建对象之后,就可以通过对象来调用下面的这些方法了:

  • void open(String method,String url,Boolen async) :创建请求
    • method :请求的方式,如:POST、GET、DELETE 等等
    • url :请求的地址
    • async :是否异步。一般都是异步的,只是他也支持同步的使用方式
  • void send(String body) :发送请求
    • body :要发送的数据
  • void setRequestHeader(String header,String value) :设置请求头
    • header :请求头的key
    • value :请求头的value
  • String getAllResponseHeaders() :获取所有响应头,返回值就是响应头数据
  • String getResponseHeader(String header) :获取响应头中指定header的值
    • header :响应头的key,返回值就是响应头的value
  • void abort() :终止请求

使用原生的方法发请求

发送GET请求
使用上面的方法,先发送一个空的GET请求:

<!-- ajax.html 文件 -->
<body>
<input type="text" placeholder="随便写点值,看看页面是否有刷新">
<input type="button" value="Ajax" />
<script>
    document.getElementsByTagName('input')[0].onclick = function () {
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open('GET', '/ajax/');
        xmlhttp.send();
    };
</script>
</body>

为了能够真正有服务响应这个请求,还得写一个处理函数:

# views.py 文件

def ajax(request):
    return render(request, 'ajax.html')

打开控制台,在网络里点击按钮触发事件后会看到我们发送的请求。点击这个请求可以看到标头。还有正文,正文里的响应正文返回的就是整个页面的html。并且整个过程里页面也是不会刷新的。

发送POST请求
上面发送的是GET请求,如果要发送POST请求,不只是要改一下method参数,还必须设置一下请求头:

xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset-UTF-8');

另外还会有csrf的问题,csrf_token可以放到表单里,另外也可以设置到请求头中。

xmlhttp.setRequestHeader('X-CsrfToken', "{{ csrf_token }}");

之前一直用{% csrf_token %},这是生成一个html,这里使用的是{{ csrf_token }},直接就是token的字符串。
另外,客户端的Cookie里也会有一个csrf的值,看下来和{{ csrf_token }}的值是不同的,但是获取过来再同样放到请求头里也是可以通过csrf验证的:

xmlhttp.setRequestHeader('X-CsrfToken', getCookie('csrftoken'));
// 这里需要一个getCookie方法,有很多的实现方式,比如下面用正则匹配的
function getCookie(name) {
    var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
    if(arr=document.cookie.match(reg)){
        return arr[2];
    } else {
        return null;
    }
}

XmlHttpRequest对象的主要属性

  • Number readyState :状态值
    • 0-未初始化,尚未调用open()方法;
    • 1-启动,调用了open()方法,未调用send()方法;
    • 2-发送,已经调用了send()方法,未接收到响应;
    • 3-接收,已经接收到部分响应数据;
    • 4-完成,已经接收到全部响应数据;
  • Function onreadystatechange :当readyState的值改变时自动触发执行其对应的函数(回调函数)
  • String responseText :服务器返回的数据
  • XmlDocument responseXML :服务器返回的数据(Xml对象)
  • Number states :状态码(整数),如:200、404 等等
  • String statesText :状态文本(字符串),如:OK、NotFound 等等,对应上面的状态码的文字说明

补充一个知识点:关于状态码和状态文本,使用HttpResponse返回的时候也是可以设置的:

return HttpResponse(json.dumps(ret), status=404, reason="Not Found")

并且一般这个状态码返回的往往都是200,因为即使后台有错误,我们捕获或者验证处理了,之后还是会正常返回数据的。如果需要用到这种状态码,就像上面一样返回的时候带上status参数设置状态码。或者我们不要这种通用的状态码,而是在我们自己写的ret的字典里,也搞一套规则表示应用返回的状态信息。两种用法都有人用,而且貌似自己搞一套的更多。

兼容性的问题

解决兼容性的问题,只需要解决这一句代码就好了 var xmlhttp = new XMLHttpRequest(); 如果有这个对象,那么就使用这个对象,如果没有这个对象,就用另外一个对象。下面的function就是提供了一个返回正确的对象的方法:

<script>
    function GetXmlhttp(){
        var xmlhttp = null;
        if(XMLHttpRequest){
            xmlhttp = new XMLHttpRequest();
        }else{
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        return xmlhttp;
    }
    // 使用的时候就不是创建对象了,而是通过上面的方法获取到对象
    // var xmlhttp = GetXmlhttp();
</script>

下面3句的效果是一样的,是讲兼容性的时候顺带提到的。即下面的3种方式引用都是可以的。

> XMLHttpRequest
< function XMLHttpRequest() { [native code] }: 
> window.XMLHttpRequest
< function XMLHttpRequest() { [native code] }: 
> window['XMLHttpRequest']
< function XMLHttpRequest() { [native code] }: 

jQuery 的 Ajax

原生方法帮助我们了解原理,使用的话还是jQuery会方便的多。

回调函数的参数-获取原生的XMLHttpRequest对象

之前使用回调函数success的时候,只用到了一个参数。这个回调函数最多是有3个参数的:

  1. 如果只传递一个参数,表示只请求服务器响应的文本信息,这样可以根据需求在服务器设置一个json格式的文本信息,客户端就可以直接获取到服务端的数据。
  2. 如果传递两个参数,则在第一个参数的基础上,增加了一个状态参数。比如:会返回字符串"success",貌似也没什么用。
  3. 如果传递三个参数,则第三个参数就是完整的ajax相应的状态信息。就是XMLHttpRequest对象,拿来就能按原生的方法来操作。
    下面上传文件的小节里会有例子

伪Ajax请求

由于HTML标签的iframe标签具有局部加载内容的特性,所以可以使用其来伪造Ajax请求。

iframe标签

在标签内部加上一个src的属性,就会在这个元素的内部创建包含另外一个文档的内联框架(就是嵌套一个网页):

<body>
<iframe src="http://blog.51cto.com/steed"></iframe>
<h2>注意带上前面的http://</h2>
<h2>举例:http://blog.51cto.com/steed</h2>
<label for="url">URL:</label><input type="text" id="url">
<input type="button" value="发送iframe请求" onclick="iframeRequest();">
<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script>
    function iframeRequest() {
        var url = $('#url').val();
        $('iframe').attr('src', url);
    }
</script>
</body>

上面还附带了一个方法,跟上input框的url,刷新iframe标签里嵌套的页面。但是页面整体是不刷新的,只有嵌套框架的内部会变化。上面的例子要说明的问题是:iframe标签也可以实现不刷新页面发送请求并且拿到返回的数据(偷偷的发请求)。
接下来,在上面的基础上,现在在form里写一个iframe标签,并且通过target属性和iframe的name相关联。原本页面上的form请求,现在都在iframe里实现了不刷新页面的提交和数据返回:

<form action="/ajax/" method="POST" target="ifm">
    {% csrf_token %}
    <iframe name="ifm"></iframe>
    <input type="text" name="username" />
    <input type="submit" />
</form>

下面是对应的处理函数:

# views.py 文件

import time
def ajax(request):
    if request.method == 'GET':
        return render(request, 'ajax.html')
    elif request.method == 'POST':
        ret = {'code': True, 'data': request.POST.get('username')}
        time.sleep(3)
        return HttpResponse(json.dumps(ret))

获取返回值

上面虽然返回了值,但是是在页面上显示的,如何拿到这些数据。iframe内部是一个完整的Document对象,这里需要使用一个特殊的方法才能取到里面的值。另外,iframe内部是在提交并且返回数据只会才会有我们需要的值的,提交之后立刻获取也是获取不到的,需要等到数据返回后才能获取到。上面的处理函数里加了一个sleep,效果更佳明显。
获取数据的时机
iframe的数据加载完成后,会触发一个onload事件,给onload事件绑定一个函数,此时再去获取,就能获取到标签内部最新的值。不过这样还不完美,第一次加载页面的时候也会触发onload事件,要解决这个问题,需要为submit绑定事件,通过submit事件来给iframe标签绑定onload事件:

<script>
    document.getElementsByTagName('form')[0].onsubmit = function () {
        document.getElementsByTagName('iframe')[0].onload = function () {
            alert(123)
        };
    }
</script>

获取数据的方法
既然iframe内部是一个DOM对象,使用 .contentWindow.document 就是内层的DOM对象。之后就是之前学习的知识了:

<script>
    document.getElementsByTagName('form')[0].onsubmit = function () {
        document.getElementsByTagName('iframe')[0].onload = function () {
            var text = this.contentWindow.document.getElementsByTagName('body')[0].innerText;
            alert(text);
        };
    }
</script>

如果是用jQuery的话,就使用jQuery的方法:

<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script>
    $('form').submit(function () {
        $('iframe').load(function () {
            var text = $(this).contents().find('body').text();
            alert(text);
        })
    })
</script>

上传文件

之前通过Ajax发送的都是普通数据。发送普通数据的时候,推荐还是用jQuery,退而求其次是用原生的,用伪Ajax并不方便。
下面看看不普通的数据,就是上传文件。

好看的上传按钮

下面的input是系统自带的上传按钮:

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .upload{display: inline-block; padding: 10px; background-color: blue; color: white;}
    </style>
</head>
<body>
<input type="file" id="file" name="file" />
<a class="upload">上传</a>
</body>

上传按钮在不用的浏览器里看到的样子也是不同的,并且样式并不能完全的按自己的需要来定制。如果要做一个好看的上传按钮,需要特殊处理一下。

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        div.file{position: relative; width: 100px; height: 50px; line-height: 50px;}
        div.file>#file{position: absolute; width:100%; height:100%; z-index: 20; opacity: 0;}
        div.file>.upload{position: absolute; width:100%; height:100%; z-index: 10;
            display: inline-block; background-color: blue; color: white; text-align: center;}
    </style>
</head>
<body>
<div class="file">
    <input type="file" id="file" name="file" />
    <a class="upload">上传</a>
</div>
</body>

上面的思路就是,把默认的input和我们的标签重叠。把默认的input放在上面,但是透明度设置为0就是看不见。把我们自定制的标签放在下面,但是上层由于全透明看不见,看到的效果就是我们自定制标签的效果。但是点击的效果还是点击了默认的input按钮,因为它才是真正在上层的元素,只是看不见而已。
要自定制好看的上传按钮,基本都是基于这个方式来实现的。

FormData 对象

这里需要先引入一个FormData对象。FormData对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。这个对象有一个append()方法,为当前的FormData对象添加键值对:

void append(String name, String value);
void append(String name, Blob value, optional String filename);

第一种用法就是传入2个字符串,前面是Key,后面是字符串。
第二种用法是,第二个参数传入一个Blob对象,即一个不可变、原始数据的类文件对象。简单理解成文件对象就好了,在上面的例子中,通过 var file_obj = document.getElementById('file').files[0]; 就能获取到这个文件对象。
最后还有一个可选的(optional)第三个参数,指定文件的文件名。

原生Ajax上传

页面的代码如下,主要看js的部分:

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        div.file{position: relative; width: 100px; height: 50px; line-height: 50px;}
        div.file>#file{position: absolute; width:100%; height:100%; z-index: 20; opacity: 0;}
        div.file>.upload{position: absolute; width:100%; height:100%; z-index: 10;
            display: inline-block; background-color: blue; color: white; text-align: center;}
    </style>
</head>
<body>
<div class="file">
    <input type="file" id="file" name="file" />
    <a class="upload">上传</a>
</div>
<input type="button" value="提交" id="btn" />
<script>
    document.getElementById('btn').onclick = function () {
        var file_obj = document.getElementById('file').files[0];
        var form_data = new FormData();
        form_data.append('username', 'root');
        form_data.append('file', file_obj);
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open('POST', '/upload/');
        xmlhttp.setRequestHeader('X-CsrfToken', "{{ csrf_token }}");
        // form_data.append('csrfmiddlewaretoken', "{{ csrf_token }}");  // 也可以加在form里
        xmlhttp.onreadystatechange = function () {
            if (xmlhttp.readyState === 4){
                var obj = JSON.parse(xmlhttp.responseText);
                console.log(obj);
            }
        };
        xmlhttp.send(form_data)
    }
</script>
</body>

对应后端的处理函数:

# views.py 文件

def upload(request):
    if request.method == 'GET':
        return render(request, 'upload.html')
    elif request.method == 'POST':
        username = request.POST.get('username')
        file_obj = request.FILES.get('file')
        print(type(file_obj), file_obj)  # 从这里看到已经收到文件了
        print(file_obj.__dict__)  # 看看有哪些属性,比如:文件名、大小、文件类型
        ret = {'code': True, 'data': username}
        # 保存文件
        with open(file_obj.name, 'wb') as file:
            for item in file_obj.chunks():
                file.write(item)
        return HttpResponse(json.dumps(ret))

jQuery 的 Ajax 上传

只有js的部分和上面不同:

<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script>
    $('#btn').click(function () {
        var file_obj = $(':file')[0].files[0];
        var form_data = new FormData();
        form_data.append('username', 'root');
        form_data.append('file', file_obj);
        $.ajax({
            url: '/upload/',
            type: 'POST',
            data: form_data,
            headers: {'X-CsrfToken': "{{ csrf_token }}"},
            processData: false,  // 上传文件必须要有这句,告诉jQuery不要对data进行加工
            contentType: false,  // 上传文件必须要有这句,告诉jQuery不要设置contentType
            success: function (data, textStatus, jqXHR) {
                console.log(data);
                console.log(textStatus);
                console.log(jqXHR);
            }
        })
    })
</script>

伪Ajax上传文件

上面使用Ajax上传文件,都必须依赖 FormDate 对象。但是这个对象不是所有浏览器都支持的,没错,还是老版的IE。要考虑兼容性问题,就需要用到这里的伪Ajax。
使用伪造Ajax上传文件和上传一般的内容几乎没什么差别,加一个 type="file" 的input框。
注意:form标签要上传文件,需要加上 enctype="multipart/form-data" 这个属性

<form action="/upload/" method="POST" target="ifm" enctype="multipart/form-data">
    {% csrf_token %}
    <iframe name="ifm"></iframe>
    <input type="text" name="username" />
    <input type="file" name="file">
    <input type="submit" />
</form>
<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script>
    $('form').submit(function () {
        $('iframe').load(function () {
            var text = $(this).contents().find('body').text();
            var obj = JSON.parse(text);
            console.log(obj);
        })
    })
</script>

上面还多了一个iframe的大框,设置 style="display: none;" 即可。剩下的上传按钮的美化方法应该是一样的。
这个方法的兼容性是最高的,所以伪Ajax在上传文件的应用场景中是推荐使用的方法。

生成预览

如果上传的文件是张图片,可以生成预览。把上传的图片存放到一个存放静态文件的目录里,setting.py里也设置好静态文件的目录。比如:

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

这里预览的图片是从服务器上获取的。也就是先把图片上传的服务器,然后服务器端返回文件在服务器上的路径给预览,预览再通过路径拿到服务器端的文件,最后显示在页面上。
处理函数修改一下,默认是保存到根目录的,现在保存到专门的目录里:

# views.py 文件

import os

def upload(request):
    if request.method == 'GET':
        return render(request, 'upload.html')
    elif request.method == 'POST':
        username = request.POST.get('username')
        file_obj = request.FILES.get('file')
        # print(type(file_obj), file_obj)  # 从这里看到已经收到文件了
        # print(file_obj.__dict__)  # 看看有哪些属性,比如:文件名、大小、文件类型
        img_path = os.path.join('static/imgs/', file_obj.name)  # 生成文件保存的路径
        ret = {'code': True, 'data': username, 'img': img_path}  # 返回的信息要有文件的路径
        # 保存文件
        with open(img_path, 'wb') as file:
            for item in file_obj.chunks():
                file.write(item)
        return HttpResponse(json.dumps(ret))

主要的变化就是生成了文件保存的路径,另外将路径返回给客户端。
下面是html的完整代码:

<div id="preview"></div>
<form action="/upload/" method="POST" target="ifm" enctype="multipart/form-data">
    {% csrf_token %}
    <iframe name="ifm" style="display: none;"></iframe>
    <input type="text" name="username" />
    <input type="file" name="file">
    <input type="submit" />
</form>
<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script>
    $('form').submit(function () {
        $('iframe').load(function () {
            var text = $(this).contents().find('body').text();
            var obj = JSON.parse(text);
            // console.log(obj);
            var imgTag = document.createElement('img');
            imgTag.src = "/" + obj.img;  // 这里注意前面需要添加个'/'
            $('#preview').empty();  // 可能有上次的预览,要先清空
            $('#preview').append(imgTag);
        })
    })
</script>

上面预设了一个图片预览的div。Ajax请求返回后,获取到其中的图片路径(这个路径应该是直接拼接到“127.0.0.1:8000”后面就能访问到图片的)。创建一个img标签,设置好src,然后加到预览的div里。加进去之前,要把div清空一下,因为有上一次预览加进去的img标签

一步提交

上面的上传文件都是分两步上传的,首先是input[type='file']标签选择文件,然后是submit或者button按钮绑定事件来进行提交。
这里也可以不要后面的submit或者button按钮,为input[type='file']绑定一个onchange事件触发一个form表单的submit事件,或者是替换到原来button按钮的onclick事件。前一种的实现只要加上下面的这个事件绑定:

<script>
    $(':file').change(function () {
        $('form').submit();
    });
<script>

补充

再来补充点相关的小点

处理函数识别AJAX请求

如下面的例子,使用 is_ajax() 方法就可以识别出请求是否是AJAX请求了:

def ajax_test(request):
    if request.is_ajax():
        message = "This is ajax"
    else:
        message = "Not ajax"
    return HttpResponse(message)

上传文件的插件推荐

这是一个可以拖拽上传文件的插件, dropzone.js 。
官网:http://www.dropzonejs.com/

下载文件

转自国外:http://djangosnippets.org/snippets/365/

import os, tempfile, zipfile
from django.http import HttpResponse
from django.core.servers.basehttp import FileWrapper

def send_file(request):
    """                                                                         
    Send a file through Django without loading the whole file into              
    memory at once. The FileWrapper will turn the file object into an           
    iterator for chunks of 8KB.                                                 
    """
    filename = __file__ # Select your file here.                                
    wrapper = FileWrapper(open(filename, 'rb'))
    response = HttpResponse(wrapper, content_type='text/plain')
    response['Content-Length'] = os.path.getsize(filename)
    return response

def send_zipfile(request):
    """                                                                         
    Create a ZIP file on disk and transmit it in chunks of 8KB,                 
    without loading the whole file into memory. A similar approach can          
    be used for large dynamic PDF files.                                        
    """
    temp = tempfile.TemporaryFile()
    archive = zipfile.ZipFile(temp, 'w', zipfile.ZIP_DEFLATED)
    for index in range(10):
        filename = __file__ # Select your files here.                           
        archive.write(filename, 'file%d.txt' % index)
    archive.close()
    wrapper = FileWrapper(temp)
    response = HttpResponse(wrapper, content_type='application/zip')
    response['Content-Disposition'] = 'attachment; filename=test.zip'
    response['Content-Length'] = temp.tell()
    temp.seek(0)
    return response

之后讲到项目,用的下载文件的方法,也是抄的这段代码。