脚本化http
超文本传输协议(HyperText Transfer Protocol,HTTP)规定Web浏览器如何从Web服务器获取文档和向Web服务器提交表单内容,以及Web服务器如何响应这些请求和提交。
Web浏览器会处理大量的HTTP,通常,HTTP并不在脚本的控制下,只是当用户单击链接、提交表单和输入URL时才发生。
但是,用JavaScript代码操作HTTP是可行的。当脚本设置window对象的location属性或调用表单对象的submit()方法时,都会初始化HTTP请求。
术语Ajax描述了一种主要使用脚本操作HTTP的Web应用架构。Ajax应用的主要特点是使用脚本操作HTTP和Web服务器进行数据交换,不会导致页面重载。避免页面重载的能力使Web应用更像传统的桌面应用。
Comet是使用脚本操纵HTTP的Web应用架构相关术语。在某种意义上Comt和Ajax相反,在Comet中,Web服务器发起通信并异步发送数据到客户端。如果Web应用需要响应服务端发送的消息,则它会使用Ajax技术发送或请求数据。在Ajax中,客户端从服务端”拉”数据。而在Comet中,服务器端向客户端”推”数据。Comet海波阔其他名词,(“服务器推”、”Ajax推”和”HTTP流”)
使用XMLHttpRequest
浏览器在XMLHttpRequest类上定义了它们的HTTP API。这个类的每个实例都表示一个独立的请求/响应对,并且这个对象的属性和方法允许指定请求细节和摄取响应数据。
当然,使用这个HTTP API必须要做的第一件事就是实例化XMLHttpRequest对象:
var request = new XMLHttpRequest();
一个HTTP请求由4个部分组成:
- HTTP请求方法或”动作”(verb)
- 正式请求的URL
- 一个可选的请求头集合,其中可能包括身份验证信息
- 一个可选的请求体
服务器返回的HTTP响应包含3部分:
- 一个数字和文字组成的状态码,用来显示请求的成功和失败
- 一个响应头集合
- 响应主体
HTTP的基础请求/响应架构非常简单并且易于使用。但在实践中会有各种各样随之而来的复杂问题:客户端和服务区交换cookie,服务区重定向浏览器到其他服务器,缓存某些资源而剩下的不缓存,某些客户端通过代理通过代理服务器发送所有的请求等。 XMLHttpRequest不是协议级的HTTP API而是浏览器级的API。浏览器需要考虑cookie、重定向、缓存和代理,但代码只需要担心请求和响应。
指定请求
-
创建XMLHttpRequest对象之后,发起HTTP请求的下一步是调用XMLHttpRequest对象的open()方法去指定这个请求的两个必需部分:方法和URL。
request.open("GET",//开始一个HTTP GET请求 "data.csv");//URL的内容
-
open()的第一个参数指定HTTP方法或动作。这个字符串不区分大小写,但通常大家用大写字母来匹配HTTP协议。
“GET”和”POST”方法得到了广泛的支持。
“GET”用于常规请求,它适用于当URL完全指定请求资源,当请求对服务器没有任何副作用以及当服务器的响应是可缓存时。
“POST”方法常用于HTML表单。它在请求主体中包含额外数据(表单数据),且这些数据常存储在服务器数据库中(副作用)。
除了这两个之外,XMLHttpRequest的规范还允许把”DELETE”、”HEAD”、”OPTIONS”和”PUT”作为open()的第一个参数。
-
open()的第二个参数时URL,它时请求的主题。这时相对于文档的URL,这个文档包含调用open()的骄傲本。如果指定绝对URL、协议、主机和端口通常必须匹配所在文档的对应内容:跨域请求通常会报错。
-
-
如果有请求头的话,请求进程的下一个步骤时设置它。例如,POST的请求需要”Content-Type”指定请求主题的MIME类型:
request.setRequestHeader("Content-Type","text/plain");
你不能自己指定”Content-Length”、”Date”、”Referer”或”User-Agent”头,XMLHttpRequest将自动添加这些头防止伪造它们。类似的XMLHttpRequest对象自动处理cookie、连续时间、字符集和编码判断
-
使用XMLHttpRequest发起请求的最后一步是指定可选的请求猪蹄并向服务器发送它。使用send()方法实现 request.send(null);
GET请求绝对没有主体,所以应该传递null或省略这个参数。POST的请求通常拥有主体,同时它应该匹配使用setRequestHeader()指定的”Content-Type”头。
顺序问题
HTTP请求的各部分有指定顺序:
- 请求方法和URL首先到达
- 然后请求头
- 最后是请求主体
XMLHttpRequest实现通常直到调用send()方法才开始启动网络。但XMLHttpRequest API设计似乎使每个方法都将写入网络流。这意味着调用XMLHttpRequest方法的顺序必须撇陪HTTP请求的架构。例如setRequestHeader()必须调用在必须在open()之后,send()之前
例子:POST方法发送纯文本给服务器
function postMessage(msg){
var request = new XMLHttpRequest();//新请求
request.open("POST","/log.php");//用POST向服务器端发送脚本
//用请求主体发送纯文本消息
request.setRequestHeader("Content-Type",//请求主体是纯文本
"text/plain;charset=UTF-8");
request.send(msg);//把msg作为请求主体发送
//请求完成,我们将忽略任何响应和任何错误
}
取得响应
一个完整的HTTP响应由状态码、响应头集合和响应主体组成。这些都可以通过XMLHttpRequest对象的属性和方法使用:
-
status和statusText属性以数字和文本的形式返回HTTP状态码。这些属性保存标准的HTTP值,像200和”OK”表示成功请求,404和”Not Found”表示URL不能匹配服务器上的任何资源。
-
使用getResponseHeader()和getAllResponseHeader()能查询响应头。XMLHttpRequest会自动处理cookie:它会从getAllResponseHeaders()头返回集合中过滤掉cookie头,而去过给getResponseHeader()传递”Set-Cookie”和”Set-Cookie2”则返回null。
-
响应主体可以从responseText属性中得到文本形式的,从responseXML属性中得到Document形式。
XMLHttpRequest对象通常异步使用:发送请求后,send()方法立即返回,知道响应返回,前面列出的响应方法和属性才有效。
为了在响应准备就绪时得到通知,必须监听XMLHttpRequest对象上的readystatechange事件。
readyState是一个整数,它指定了HTTP请求的状态,第一列的符号时XMLHttpRequest构造函数定义的常量。
XMLHttpRequest的readyState值
常量 值 含义
UNSENT 0 open()尚未调用
OPENED 1 open()已经调用
HEADERS_RECEIVED 2 接受到头信息
LOADING 3 接收到响应主体
DONE 4 响应完成
示例:定义了getText()函数来演示如何监听readystatechange事件。
// 发出一个HTTP GET请求以获得指定URL的内容
// 当响应成功到达,验证它是否是纯文本
// 如果是,把它传递给指定回调函数
function getText(url, callback) {
var request = new XMLHttpRequest(); // 新建请求
request.open("GET", url); // 指定待获取的URL
request.onreadystatechange = function() { // 定义事件处理程序
// 如果请求完成,则它是成功的
if (request.readyState === 4 && request.status === 200) {
var type = request.getResponseHeader("Content-Type");
if (type.match(/^text/)) // 确保响应式文本
callback(request.responseText); // 把它传递给回调函数
}
};
request.send(null); // 立即发送请求
}
-
同步响应
由于其本身的性质,异步处理HTTP响应是最好的方式。然而,XMLHttpRequest也支持同步响应。如果把false作为第三个参数传递给open(),那么send()方法将阻塞直到请求完成。
// 发出一个HTTP GET请求以获得指定URL的内容 // 返回响应文本,或如果请求不成功或响应不是文本就报错 function getTexSynct(url) { var request = new XMLHttpRequest(); // 新建请求 request.open("GET", url); // 传递false实现同步 request.sen(null); // 立即发送请求 // 如果请求不是200 OK,就报错 if (request.status === 200) throw new Error(request.statusText); //如果类型错误,就报错 var type = request.getResponseHeader("Content-Type"); if(!type.match(/^text/)) throw new Error("Expected textual response;got:"+type); return request.responseText; }
同步请求是吸引人的,但应该避免使用它们,客户端JavaScript是单线程的,当send()方法阻塞时,它通常会导致整个浏览器UI冻结。如果连接到服务器响应慢,那么用户的浏览器将冻结。
-
响应解码
在前面的示例中,我们假设服务器使用像”text/plain”、”text/html”或”text/css”这样的MIME类型发送文本响应,然后我们使用XMLHttpRequest对象的responseText属性得到它。
如果服务器想发送诸如对象或数组这样的结构化数据作为其响应,它应该传输JSON编码的字符串数据。当接受它时,可以把responseText属性传递给JSON.parse()。
示例解析HTTP响应
// 发起HTTP GET响应以获取指定URL的内容 // 当响应到达时,把它以解析后的XML Document对象、解析后的JSON对象 // 或字符串形式传递给回调函数 function get(url, callback) { var request = new XMLHttpRequest(); // 创建新请求 request.open("GET", url); // 指定待获取的URL request.onreadystatechange = function() { // 定义事件监听器 // 如果请求完成且成功 if (request.readyState === 4 && request.status === 200) { // 获取响应类型 var type = request.getResponseHeader("Content-Type"); // 检查类型,这样我们不能在将来得到HTML文档 if (type.indexOf("xml") !== -1 && request.responseXML) callback(request.responseXML); // Document对象相应 else if (type === "application/json") callback(JSON.parse(request.responseText)); // JSON响应 else callback(request.responseText); // 字符串响应 } }; request.send(null); // 立即发送请求 }
编码请求主体
HTTP POST请求包括一个请求主体,它包含客户端传递给服务器的数据。我们通常使用HTTP请求发送的都是更复杂的数据。
-
表单编码的请求
考虑HTML表单。当用户提交表单时,表单中的数据(每个表单元素的名字和值)编码到一个字符串中并随请求发送。像这样:
name=minchao&sex=man 表单数据编码格式是一个正式的MIME类型: application/x-www-form-urlencoded
当POST方法提交表单数据时,必须设置”Content-Type”请求头为这个值。这种编码并不要HTML表单。在Ajax应用中,你希望发送给服务器的很可能时一个JavaScript对象。
{ name:"minchao", sex:"man" } 用于HTTP请求的编码对象 /** * 编码对象的属性, * 如果它们是来自HTML表单的名/值对,使用application/x-wwww-form-urlencode格式 */ function encodeFormData(data) { if (!data) return ""; // 一直返回字符串 var pairs = []; // 为了保存名值对 for(var name in data) { // 遍历每个名字 if (!data.hasOwnProperty(name)) continue; // 跳过继承属性 if (typeof data[name] === "function") continue; // 跳过方法 var value = data[name].toString(); // 把值转换成字符串 name = encodeURIComponent(name.replace(" ", "+")); // 编码名字 value = encodeURIComponent(value.replace(" ", "+")); // 编码值 pairs.push(name + "=" + value); // 记住名=值对 } return pairs.join('&'); // 返回使用&连接的名/值对 }
使用表单编码数据发起一个HTTP POST请求
function postData(url, data, callback) { var request = new XMLHttpRequest(); request.open("POST", url); // 对指定URL发生POST请求 request.onreadystatechange = function() { // 简单的事件处理程序 if (request.readyState === 4 && callback) // 当响应完成 callback(request); // 调用回调函数 }; request.setRequestHeader("Content-Type", // 设置 Content-Type "application/x-www-form-urlencoded"); request.send(encodeFormData(data)); // 发送表单数据 }
使用表单编码数据发起HTTP GET请求
function getData(url, data, callback) { var request = new XMLHttpRequest(); request.open("GET", url + // 通过添加编码数据获取指定的url "?" + encodeFormData(data)); request.onreadystatechange = function() { // 简单事件处理程序 if (request.readyState === 4 && callback) callback(request); }; request.send(null); // 发送请求 }
-
JSON编码的请求
在POST请求主体中使用表单编码时常见惯例。
function postJSON(url, data, callback) { var request = new XMLHttpRequest(); request.open("POST", url); // 对指定URL发送POST请求 request.onreadystatechange = function() { // 简单是事件处理程序 if (request.readyState === 4 && callback) // 当响应完成时 callback(request); // 调用回调函数 }; request.setRequestHeader("Content-Type", "application/json"); request.send(JSON.stringify(data)); }
-
XML编码的请求
XML有时也用于数据传输的编码。JavaScript对象的用表单编码或JSON编码版本表达的。 略
-
上传文件
HTML表单的特性之一时当用户通过< input type=”file”>元素选择文件时,表单将在它产生的POST请求主体中发送文件内容。HTNL表单始终能上传文件,但到目前为止,它还不能使用XMLHttpRequest API做相同的事。
使用HTTP POST请求上传文件
//查找有data-upload属性的全部<input tyep="file">元素 //并注册onchange事件处理程序 //这样任何选择的文件都会自动通过POST方法发送到指定的"uploadto"URL //服务器的响应是忽略的 whenReady(function() { // 当文档准备就绪时运行 var elts = document.getElementsByTagName("input"); // 所有的input元素 for(var i = 0; i < elts.length; i++) { // 遍历它们 var input = elts[i]; if (input.type !== "file") continue; // 跳过所有非文件上传元素 var url = input.getAttribute("data-uploadto"); // 获取上级URL if (!url) continue; // 跳过任何没有URL的元素 input.addEventListener("change", function() { // 当用户选择文件时 var file = this.files[0]; // 假设单个文件选择 if (!file) return; // 如果没有文件,不做任何事情 var xhr = new XMLHttpRequest(); // 创建新请求 xhr.open("POST", url); // 向这个URL发送POST请求 xhr.send(file); // 把文件作为主体发送 }, false); } });
-
multipart/form-data请求
当HTML表单同时包含文件上传元素和其他元素时,浏览器不能使用普遍的表单编码而必须使用称为”multipart/form-data”的特殊Content-Type来用POST方法提交表单。
使用POST方法发送multipart/form-data请求主体
function postFormData(url, data, callback) { if (typeof FormData === "undefined") throw new Error("FormData is not implemented"); var request = new XMLHttpRequest(); // 新HTTP请求 request.open("POST", url); // 对指定URL发送POST请求 request.onreadystatechange = function() { // 简单的事件处理程序 if (request.readyState === 4 && callback) // 当响应完成时 callback(request); // 调用回调函数 }; var formdata = new FormData(); for(var name in data) { if (!data.hasOwnProperty(name)) continue; // 跳过继承的属性 var value = data[name]; if (typeof value === "function") continue; // 跳过方法 // 每个属性变成请求的一部分 // 这里允许File对象 formdata.append(name, value); // 作为一部分添加名/值对 } // 在multipart/form-data请求主体中发送名值对 // 每对都是请求的一部分,注意,当传入FormData对象时 // sen()会自动设置Content-Type头 request.send(formdata); }
HTTP进度事件
在之前的示例中,使用readystatechange事件探测HTTP请求的完成。
常见的progress事件处理程序一样使用upload事件处理程序,对于XMLHttpRequest对象x,设置x.onpressgress以监控响应的下载进度,并且设置x.upload.onprogress以监控请求的上传进度。
// 查找所有含有"fileDropTarget"类的元素
// 并注册DnD事件处理程序使它们能响应文件的拖拽
// 当文件放下时,上传它们到data-uploadto属性
whenReady(function() {
var elts = document.getElementsByClassName("fileDropTarget");
for(var i = 0; i < elts.length; i++) {
var target = elts[i];
var url = target.getAttribute("data-uploadto");
if (!url) continue;
createFileUploadDropTarget(target, url);
}
function createFileUploadDropTarget(target, url) {
// 跟踪当前是否正在上传,因此我们能拒绝放下
// 我们可以处理多个并发上传
// 但对这个例子使用进度通知太困难了
var uploading = false;
console.log(target, url);
target.ondragenter = function(e) {
console.log("dragenter");
if (uploading) return; // 如果正忙,忽略拖放
var types = e.dataTransfer.types;
if (types &&
((types.contains && types.contains("Files")) ||
(types.indexOf && types.indexOf("Files") !== -1))) {
target.classList.add("wantdrop");
return false;
}
};
target.ondragover = function(e) { if (!uploading) return false; };
target.ondragleave = function(e) {
if (!uploading) target.classList.remove("wantdrop");
};
target.ondrop = function(e) {
if (uploading) return false;
var files = e.dataTransfer.files;
if (files && files.length) {
uploading = true;
var message = "Uploading files:<ul>";
for(var i = 0; i < files.length; i++)
message += "<li>" + files[i].name + "</li>";
message += "</ul>";
target.innerHTML = message;
target.classList.remove("wantdrop");
target.classList.add("uploading");
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
var body = new FormData();
for(var i = 0; i < files.length; i++) body.append(i, files[i]);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
target.innerHTML = message +
Math.round(e.loaded/e.total*100) +
"% Complete";
}
};
xhr.upload.onload = function(e) {
uploading = false;
target.classList.remove("uploading");
target.innerHTML = "Drop files to upload";
};
xhr.send(body);
return false;
}
target.classList.remove("wantdrop");
}
}
});
中止请求和超时
可以通过调用XMLHttpRequest对象的abort()方法来取消正在进行的HTTP请求。abort()方法在所有的XMLHttpRequest版本和XHR2中可用。
// 发起HTTP GET请求获取指定的URL的内容
// 如果响应成功到达,传入responseText给回调函数
// 如果响应在timeout毫秒内没有到达,中止这个请求
// 浏览器可能在abort()后触发"readstatechange"
// 如果时部分请求结果到达,甚至可能设置status属性
// 所以需要设置一个标记,当部分且超时的响应到达时不会调用回调函数
// 如果使用load事件就没有这个风险
function timedGetText(url, timeout, callback) {
var request = new XMLHttpRequest(); // 创建新请求
var timedout = false; // 是否超时
// 启动计时器,在timeout毫秒后将中止请求
var timer = setTimeout(function() { // 如果触发,启动一个计时器
timedout = true; // 如果标记
request.abort(); // 然后中止请求
},
timeout); // 中止请求之前的时长
request.open("GET", url); // 获取指定的URL
request.onreadystatechange = function() { // 定义事件处理程序
if (request.readyState !== 4) return; // 忽略未完成的请求
if (timedout) return; // 忽略中止请求
clearTimeout(timer); // 取消等待的超时
if (request.status === 200) // 如果请求成功
callback(request.responseText); // 把response传给回调函数
};
request.send(null); // 立即发送请求
}
跨域HTTP请求
作为同源策略的一部分,XMLHttpRequest对象通常仅可以发起和文档具有相同服务器的HTTP请求。
借助< script>发送HTTP请求:JSON
< script>元素可以作为一种Ajax传输机制:只须设置< script>元素的src属性,然后浏览器就会发送一个HTTP请求以下载src属性所指向的URL。
脚本和安全性
为了使用< script>元素进行Ajax传输,必须允许Web页面可以执行远程服务器发送过来的任何JavaScript代码,这意味着对于不可信的服务器,不应该采取该技术。当与可信的服务器通信时,要提防攻击者可能进入服务器中,然后黑客会接管你的页面,运行他自己的代码,并显示任何他想要的内容。
这种使用< script>元素作为Ajax传输的技术成为JSONP,若HTTP请求所得到的响应数据时经过JSON编码的,则适合使用该技术。
// 根据指定的URL发送一个JSON请求
// 然后把解析得到的响应数据传递给回调函数
// 在URL中添加一个名为jsonp的查询参数,用于指定该请求的回掉函数的名称
function getJSONP(url, callback) {
// 为本次请求创建一个唯一的回调函数名称
var cbnum = "cb" + getJSONP.counter++; // 每次自增计数器
var cbname = "getJSONP." + cbnum; // 作为JSONP函数的属性
// 将回调函数名称以表单编码的形式添加到URL的查询部分
// 使用jsonp作为参数名,一些支持JSONP的服务
// 可能使用其他的参数名,比如callback
if (url.indexOf("?") === -1) // URL没有查询部分
url += "?jsonp=" + cbname; // 作为查询部分添加参数
else // 否则
url += "&jsonp=" + cbname; // 作为新的参数添加它
// 创建script元素用于发送请求
var script = document.createElement("script");
// 定义将被脚本执行的回调函数
getJSONP[cbnum] = function(response) {
try {
callback(response); // 处理响应数据
}
finally { // 即使回调函数或响应抛出错误
delete getJSONP[cbnum]; // 删除该函数
script.parentNode.removeChild(script); // 移除script元素
}
};
// 立即触发HTTP请求
script.src = url; // 设置脚本的URL
document.body.appendChild(script); // 把它添加到文档中
}
getJSONP.counter = 0; // 用于创建唯一回调函数名称的计数器
基于服务器端推送事件的Come技术
在服务器端推送事件的标准草案中定义了一个EventSource对象,简化了Comet应用程序的编写可以传递一个URL给EventSource()构造函数,然后在返回的实例上监听消息事件。
var ticker = new EventSource("stockprices.php");
ticker.onmessage = function(e){
var type = e.type;
var data = e.data;
//现在处理事件类型和事件的字符串数据
}
Comet架构的一个常见的应用是聊天应用,聊天客户端可以通过XMLHttpRequest向聊天室发送新的消息,也可以通过EventSource对象订阅聊天信息。