15. Ajax
15. Ajax
JavaScript 写 ajax
AJAX 不是 JavaScript 的规范,它只是一个哥们 “发明” 的缩写:Asynchronous JavaScript and XML,意思就是用 JavaScript 执行异步网络请求。
如果仔细观察一个 Form 的提交,你就会发现,一旦用户点击 “Submit” 按钮,表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个 404 页面。
这就是 Web 的运作原理:一次 HTTP 请求对应一个页面。
如果要让用户留在当前页面中,同时发出新的 HTTP 请求,就必须用 JavaScript 发送这个新请求,接收到数据后,再用 JavaScript 更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新。
最早大规模使用 AJAX 的就是 Gmail,Gmail 的页面在首次加载后,剩下的所有数据都依赖于 AJAX 来更新。
用 JavaScript 写一个完整的 AJAX 代码并不复杂,但是需要注意:AJAX 请求是异步执行的,也就是说,要通过回调函数获得响应。
在现代浏览器上写 AJAX 主要依靠 XMLHttpRequest
对象:
function success(text) {
var textarea = document.getElementById("test-response-text");
textarea.value = text;
}
function fail(code) {
var textarea = document.getElementById("test-response-text");
textarea.value = "Error code: " + code;
}
var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象
request.onreadystatechange = function () {
// 状态发生变化时,函数被回调
if (request.readyState === 4) {
// 成功完成
// 判断响应结果:
if (request.status === 200) {
// 成功,通过responseText拿到响应的文本:
return success(request.responseText);
} else {
// 失败,根据响应码判断失败原因:
return fail(request.status);
}
} else {
// HTTP请求还在继续...
}
};
// 发送请求:
request.open("GET", "/api/categories");
request.send();
alert("请求已发送,请等待响应...");
对于低版本的 IE,需要换一个 ActiveXObject
对象:
function success(text) {
var textarea = document.getElementById("test-ie-response-text");
textarea.value = text;
}
function fail(code) {
var textarea = document.getElementById("test-ie-response-text");
textarea.value = "Error code: " + code;
}
var request = new ActiveXObject("Microsoft.XMLHTTP"); // 新建Microsoft.XMLHTTP对象
request.onreadystatechange = function () {
// 状态发生变化时,函数被回调
if (request.readyState === 4) {
// 成功完成
// 判断响应结果:
if (request.status === 200) {
// 成功,通过responseText拿到响应的文本:
return success(request.responseText);
} else {
// 失败,根据响应码判断失败原因:
return fail(request.status);
}
} else {
// HTTP请求还在继续...
}
};
// 发送请求:
request.open("GET", "/api/categories");
request.send();
alert("请求已发送,请等待响应...");
如果你想把标准写法和 IE 写法混在一起,可以这么写:
var request;
if (window.XMLHttpRequest) {
request = new XMLHttpRequest();
} else {
request = new ActiveXObject("Microsoft.XMLHTTP");
}
通过检测 window
对象是否有 XMLHttpRequest
属性来确定浏览器是否支持标准的 XMLHttpRequest
。注意,不要根据浏览器的 navigator.userAgent
来检测浏览器是否支持某个 JavaScript 特性,一是因为这个字符串本身可以伪造,二是通过 IE 版本判断 JavaScript 特性将非常复杂。
当创建了 XMLHttpRequest
对象后,要先设置 onreadystatechange
的回调函数。在回调函数中,通常我们只需通过 readyState === 4
判断请求是否完成,如果已完成,再根据 status === 200
判断是否是一个成功的响应。
XMLHttpRequest
对象的 open()
方法有 3 个参数,第一个参数指定是 GET
还是 POST
,第二个参数指定 URL 地址,第三个参数指定是否使用异步,默认是 true
,所以不用写。
注意,千万不要把第三个参数指定为 false
,否则浏览器将停止响应,直到 AJAX 请求完成。如果这个请求耗时 10 秒,那么 10 秒内你会发现浏览器处于 “假死” 状态。
最后调用 send()
方法才真正发送请求。GET
请求不需要参数,POST
请求需要把 body 部分以字符串或者 FormData
对象传进去。
JS 的安全限制
上面代码的 URL 使用的是相对路径。如果你把它改为'http://www.sina.com.cn/'
,再运行,肯定报错。在 Chrome 的控制台里,还可以看到错误信息。
这是因为浏览器的同源策略导致的。默认情况下,JavaScript 在发送 AJAX 请求时,URL 的域名必须和当前页面完全一致。
完全一致的意思是,域名要相同(www.example.com
和 example.com
不同),协议要相同(http
和 https
不同),端口号要相同(默认是:80
端口,它和:8080
就不同)。有的浏览器口子松一点,允许端口不同,大多数浏览器都会严格遵守这个限制。
那是不是用 JavaScript 无法请求外域(就是其他网站)的 URL 了呢?方法还是有的,大概有这么几种:
一是通过 Flash 插件发送 HTTP 请求,这种方式可以绕过浏览器的安全限制,但必须安装 Flash,并且跟 Flash 交互。不过 Flash 用起来麻烦,而且现在用得也越来越少了。
二是通过在同源域名下架设一个代理服务器来转发,JavaScript 负责把请求发送到代理服务器:
'/proxy?url=http://www.sina.com.cn'
代理服务器再把结果返回,这样就遵守了浏览器的同源策略。这种方式麻烦之处在于需要服务器端额外做开发。
第三种方式称为 JSONP,它有个限制,只能用 GET 请求,并且要求返回 JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用 JavaScript 资源:
<html>
<head>
<script src="http://example.com/abc.js"></script>
...
</head>
<body>
...
</body>
</html>
JSONP 通常以函数调用的形式返回,例如,返回 JavaScript 内容如下:
foo("data");
这样一来,我们如果在页面中先准备好 foo()
函数,然后给页面动态加一个 <script>
节点,相当于动态读取外域的 JavaScript 资源,最后就等着接收回调了。
以 163 的股票查询 URL 为例,对于 URL:http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice,你将得到如下返回:
refreshPrice({"0000001":{"code": "0000001", ... });
因此我们需要首先在页面中准备好回调函数:
function refreshPrice(data) {
var p = document.getElementById("test-jsonp");
p.innerHTML =
"当前价格:" +
data["0000001"].name +
": " +
data["0000001"].price +
";" +
data["1399001"].name +
": " +
data["1399001"].price;
}
最后用 getPrice()
函数触发:
function getPrice() {
var js = document.createElement("script"),
head = document.getElementsByTagName("head")[0];
js.src =
"http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice";
head.appendChild(js);
}
就完成了跨域加载数据。
CORS
如果浏览器支持 HTML5,那么就可以一劳永逸地使用新的跨域策略:CORS 了。
CORS 全称 Cross-Origin Resource Sharing,是 HTML5 规范定义的如何跨域访问资源。
了解 CORS 前,我们先搞明白概念:
Origin 表示本域,也就是浏览器当前页面的域。当 JavaScript 向外域(如 sina.com)发起请求后,浏览器收到响应后,首先检查 Access-Control-Allow-Origin
是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript 将无法获取到响应的任何数据。
用一个图来表示就是:
假设本域是 my.com
,外域是 sina.com
,只要响应头 Access-Control-Allow-Origin
为 http://my.com
,或者是 *
,本次请求就可以成功。
可见,跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的 Access-Control-Allow-Origin
,决定权始终在对方手中。
上面这种跨域请求,称之为 “简单请求”。简单请求包括 GET、HEAD 和 POST(POST 的 Content-Type 类型 仅限 application/x-www-form-urlencoded
、multipart/form-data
和 text/plain
),并且不能出现任何自定义头(例如,X-Custom: 12345
),通常能满足 90% 的需求。
无论你是否需要用 JavaScript 通过 CORS 跨域请求资源,你都要了解 CORS 的原理。最新的浏览器全面支持 HTML5。在引用外域资源时,除了 JavaScript 和 CSS 外,都要验证 CORS。例如,当你引用了某个第三方 CDN 上的字体文件时:
/* CSS */
@font-face {
font-family: "FontAwesome";
src: url("http://cdn.com/fonts/fontawesome.ttf") format("truetype");
}
如果该 CDN 服务商未正确设置 Access-Control-Allow-Origin
,那么浏览器无法加载字体资源。
对于 PUT、DELETE 以及其他类型如 application/json
的 POST 请求,在发送 AJAX 请求之前,浏览器会先发送一个 OPTIONS
请求(称为 preflighted 请求)到这个 URL 上,询问目标服务器是否接受:
OPTIONS /path/to/resource HTTP/1.1
Host: bar.com
Origin: http://my.com
Access-Control-Request-Method: POST
服务器必须响应并明确指出允许的 Method:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
Access-Control-Max-Age: 86400
浏览器确认服务器响应的 Access-Control-Allow-Methods
头确实包含将要发送的 AJAX 请求的 Method,才会继续发送 AJAX,否则,抛出一个错误。
由于以 POST
、PUT
方式传送 JSON 格式的数据在 REST 中很常见,所以要跨域正确处理 POST
和 PUT
请求,服务器端必须正确响应 OPTIONS
请求。
需要深入了解 CORS 请移步 W3C 文档。
jQuery 写 Ajax
用 jQuery 的相关对象来处理 AJAX,不但不需要考虑浏览器问题,代码也能大大简化。
ajax
jQuery 在全局对象 jQuery
(也就是 $
)绑定了 ajax()
函数,可以处理 AJAX 请求。ajax(url, settings)
函数需要接收一个 URL 和一个可选的 settings
对象,常用的选项如下:
- async:是否异步执行 AJAX 请求,默认为
true
,千万不要指定为false
; - method:发送的 Method,缺省为
'GET'
,可指定为'POST'
、'PUT'
等; - contentType:发送 POST 请求的格式,默认值为
'application/x-www-form-urlencoded; charset=UTF-8'
,也可以指定为text/plain
、application/json
; - data:发送的数据,可以是字符串、数组或 object。如果是 GET 请求,data 将被转换成 query 附加到 URL 上,如果是 POST 请求,根据 contentType 把 data 序列化成合适的格式;
- headers:发送的额外的 HTTP 头,必须是一个 object;
- dataType:接收的数据格式,可以指定为
'html'
、'xml'
、'json'
、'text'
等,缺省情况下根据响应的Content-Type
猜测。
下面的例子发送一个 GET 请求,并返回一个 JSON 格式的数据:
var jqxhr = $.ajax("/api/categories", {
dataType: "json",
});
// 请求已经发送了
不过,如何用回调函数处理返回的数据和出错时的响应呢?
我们可以用链式写法来处理各种回调:
function ajaxLog(s) {
var txt = $("#test-response-text");
txt.val(txt.val() + "\n" + s);
}
$("#test-response-text").val("");
get
对常用的 AJAX 操作,jQuery 提供了一些辅助方法。由于 GET 请求最常见,所以 jQuery 提供了 get()
方法,可以这么写:
var jqxhr = $.get("/path/to/resource", {
name: "Bob Lee",
check: 1,
});
第二个参数如果是 object,jQuery 自动把它变成 query string 然后加到 URL 后面,实际的 URL 是:
/path/to/resource?name=Bob%20Lee&check=1
这样我们就不用关心如何用 URL 编码并构造一个 query string 了。
post
post()
和 get()
类似,但是传入的第二个参数默认被序列化为 application/x-www-form-urlencoded
:
var jqxhr = $.post("/path/to/resource", {
name: "Bob Lee",
check: 1,
});
实际构造的数据 name=Bob%20Lee&check=1
作为 POST 的 body 被发送。
getJSON
由于 JSON 用得越来越普遍,所以 jQuery 也提供了 getJSON()
方法来快速通过 GET 获取一个 JSON 对象:
var jqxhr = $.getJSON("/path/to/resource", {
name: "Bob Lee",
check: 1,
}).done(function (data) {
// data已经被解析为JSON对象了
});
JQ 的安全限制
jQuery 的 AJAX 完全封装的是 JavaScript 的 AJAX 操作,所以它的安全限制和前面讲的用 JavaScript 写 AJAX 完全一样。
如果需要使用 JSONP,可以在 ajax()
中设置 jsonp: 'callback'
,让 jQuery 实现 JSONP 跨域加载数据。
Ajax 模板
$.ajax({
url: 请求的地址,
data: 传递给后台的数据,
method: 请求方法(get,post),
async: 是否异步(同步:必须等待后台返回数据后程序才会继续往下走,异步:发送后不管是否成功都可以继续往下走),
beforeSend: 发送到后台前执行的函数,可以用作校验或给按钮添加样式,如果用作校验return false;则可以取消本次请求,
contentType: 默认值: "application/x-www-form-urlencoded"。发送信息至服务器时内容编码类型,
dataType:"json"(指定服务器返回的数据类型),
success: function(data) {
请求成功的函数,data是后台返回的数据,在这里对数据进行操作
},
error: function(err) {
请求失败,err是失败的内容
},
complete: function(e) {
不管成功还是失败都会调用这个函数
}
})