// 向应用程序中加入Ajax /** * 组合技术 * 语义化的(X)HTML标记 * 文档对象模型(DOM) * Javascript * XML */ /** * 在不同的浏览器之间,公共的XMLHttpRequest方法包括: * * open(method, URL[, asynchronous[, userName[, password]]]) 用于指定请求YRL,方法以及与请求相关的其他可选属性。 * * setRequestHeader(label, value) 用于以给定的label和value为请求应用一个头部信息。该方法必须在请求的open()方法之后且在send()方法之前调用。 * * send(content) 用于发送请求,可以包含可选的内容,例如POST请求的内容等。 * * abort() 用于停止当前的请求 * * getAllResponseHeaders() 返回字符串形式的完整的头部信息集合。 * getResponseHeader(label) 返回指定头部的一个单独的字符串值。 */ // 生成一次新请求 /* 要生成一次请求,需要用过浏览器能力检测来实例化一个新对象,并调用open()和send()方法: */ function stateChangeListener() { // ... } var request = false; if (window.XMLHttpRequest) { request = new window.XMLHttpRequest(); } else if (window.ActiveXObject) { request = new window.ActiveXObject('Microsoft.XMLHTTP'); } if (request) { request.onreadystatechange = stateChangeListener; request.open('GET', '/your/script/?var=value&var2=value', true); request.send(null); } // 这个例子发送了一个GET请求,而请求URL的后面还附加了几个必须的变量。 // 对于GET请求而言,sned()方法的参数应该是null。如果要执行一次POST请求,那么应该将POST作为open()的方法参数,并且通过使用send()方法在请求主体中包含变量,而不是将变量包含在URL中: request.open('POST', '/your/script/', true); request.send('var=value&var2=value'); // 处理响应 /** * XMLHttpRequest对象的属性 * readyState是一个表示下列状态的整数值 * 0: 尚未初始化 * 1: 载入中 * 2: 载入完成 * 3: 交互 * 4: 完成 * responseText是一个在响应中返回的数据的字符串表示 * responseXML是一个兼容DOM核心的文档对象(在响应是一个有效的XML文档并且设置了适当的头部信息的情况下)。 * status是一个表示请求状态的数字代码。这些数字代码由服务器生成的HTTP协议状态代码,200表示“OK”。 * statusText是与状态代码相关的一条信息。 * onreadystatechange应该包含在请求的不同readyState状态下被调用的方法。 */ // 使用指定给onreadystatechange属性的方法以及其他的请求属性,可以判断请求的状态(是否成功),以及从服务器返回的响应是什么类型。 function stateChangeListener() { // 橘亘请求的状态转换功能 switch (request.readyState) { case 1: // 载入中 break; case 2: // 载入完成 break; case 3: // 交互 break; case 4: // 完成 if (request.status === 200) { // 对request.responseText // 或request.responseXML进行处理 } else { // request.status钟可能包含某个错误代码 // 而request.statusText中则包含报告的错误信息 } break; default: break; } } /* 在任何情况下,吐过请求完全成功,则responseText属性将被响应返回的一个字符串填充。但如果响应的Content-Type头部信息是application/xml,并且响应是一个有效的XML文档,那么responseXML属性将会保存一个XML DOM文档。这种DOM表示法的用处在于,你可以使用DOM2核心方法遍历并读取responseXML中保存的文档。 假如你希望使用XML格式,就必须将响应的Content-Type头部信息设置为application/xml;否则,即使相应的是一个有效的XML文档,XMLHttpRequest对象仍然会假定响应中只包含文本。同样也必须记住,responseXML中保存的文档将被解释为XML文档,而非HTML文档,因此DOM2 HTML规范中规定的所有方法都不适用,即使文档包含有效的HTML代码也是如此。 当通过XMLHttpRequest对象发起请求时,需要牢记的重要一点是,作为请求结果而采取的操作必须通过onreadystatechange侦听器来调用。 */ // 创建你希望运行的方法 function alertAndDoWhatever(r) { // e是通过onreadystatechange // 侦听器传递进来的请求对象 alert(r.responseText()); //... } request.onreadystatechange = function () { // 当请求完成时运行相应的方法 if (request.readyState === 4 && request.status === '200') { // 请求已经成功完成,需要调用你创建的方法 alertAndDoWhatever(request); } }; // 启动请求 request.open('GET', '/yourscript/?var=value', true); // 发送请求 request.send(null); //** 当在指定给onreadystatechange属性的方法内部使用this关键字时, // this引用方法自身,而不是XMLHttpRequest对象。 // 在服务器上识别Ajax请求 /* 为了标识来自XMLHttpRequest对象的请求,可以使用请求对象的setRequestHeader()方法来发送自定义的头部信息。可以在该方法中指定任何头部信息,但却只能在启动请求事务之后并且在调用send()方法之前调用该方法: */ request.open('GET', 'yourscript/?var=value', true); request.setRequestHeader('My-Special-Header', 'AjaxRequest'); request.setRequestHeader('Sent-by', 'Jeff'); request.send(); /** * 然后就可以在服务器端检查特殊的头部信息并采取相应的操作。在PHP中,这一过程简单到只需从全局$_SERVER数组中读取头部信息 * <?php if(isset($_SERVER('HTTP_MY_SPECIAL_HEADER'))){ // 对XMLHttpRequest对象的响应 } else { // 对传统请求的响应 } ?>: */ /* 在PHP中,头部信息被保存在关联的$_SERVER全局数组中,而头部信息的名称对应着数组的键,键的格式会在名称基础上稍微有所改动,名称的文本会转换为大写,而所有连字符(-)斗会转换成下划线(_),同时,每个表示头部信息的键还以HTTP作为前缀来表示该信息来自HTTP请求。 */ // 如果你想要以 POST 方式傳送資料,則必須先將 MIME 型態改好,如下: request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); /* 同理,通过请求对象的getResponseHeader()方法可以从相应中取得一个特殊的头部信息,因而可以基于响应中的信息(如Content-Type)来修改脚本: */ switch (request.getResponseHeader('Content-Type')) { case 'text/javascript': // 俺request.responseText中包含Javascript代码处理 break; case 'text/xml': case 'application/xml': // 使用request.responseXML或将request.responseText读作XML break; case 'text/html': // 按request.responseText中包含HTML代码处理 break; default: break; } /* 取得哪种Content-Type取决于服务器的响应内容。如果服务器返回XHTML而不是HTML,那么Content-Type可能是application/xhtml+xml,而且,Content-Type头部信息中也可能包含字符编码: Content-Type: text/html; charset=ISO-8859-4 而这会导致前面的切换代码失效,因为该代码只查找text/html。 */ // 也可以使用发送带自定义头部信息的请求这种方法来验证服务器是否返回了适当的响应: request.onreadystatechange = processSpecialRequest; request.open('GET', '/yourscript/?var=value', true); request.setRequestHeader('My-Ajax0Request', 'SpecialValue'); request.send(null); // 并在响应中的另一个自定义头部信息中使用相同的值 /** * <?php * if(isset($_SERVER('HTTP_MY_AJAX_REQUEST'))) { * header('My-Ajax-Response: '.$_SERVER('HTTP_MY_AJAX_REQUEST')); * echo 'Hello'; * } * ?> */ // 请求的onreadystatechange侦听器可以在进一步处理之前检查响应中是否包含有效的头部信息: function processSpecialRequest(request) { if (request.readyState === 4) { var header = request.getResponseHeader('My-Ajax-Response'); if (header === 'SpecialValue') { // 服务器响应中包含了你发送的预期值 alert(request.responseText); } else { // 服务器响应了其他的值 } } } // 不限于GET和POST /** * open()方法可以接受如下方法: * GET: 用于从服务器取得头部信息及其他信息的请求。 * POST: 用于修改服务器上信息的请求。 * HEAD: 用于与GET相同的请求,但HEAD中只包括与请求关联的头部信息,而不包含请求的主体。 * PUT: 用于希望在服务器上某个特殊位置存储信息的请求。 * DELETE: 用于希望删除服务器上的文件或资源的请求。 * OPTIONS: 用于列出服务器上可用选项的请求。 */ /* 大多数服务器都支持GET,POST和HEAD请求,而PUT,DELETE和OPTIONS请求则通常视为等同于GET请求。 典型的GET或POST请求返回的所有信息如果并非全都需要,可以使用以上代替方法。 */ // 如果只想检查服务器上一个文件的状态,而并不想实际取得该文件,那么就可以使用HEAD方法 // 不过这样可能会因为网络原因,而不是因为缺少文件而导致不明确的响应,甚至发生错误。 request.onreadystatechange = function () { if (this.readyState === 4) { alert(this.status); } }; request.open('HEAD', '/some/file.pdf', true); request.send(null); // XML /* Ajax混合技术中的最后一部分是XML响应。作为一种数据传输机制,XML在允许开发者从DOM层次上遍历,读取和操纵相应的数据方面是值得称道的。而且,如果在处理过程中整合了一个XSLT(可扩展样式表语言转换)解析机制,那么服务器和客户端就能共享XSLT文件以确保在服务器和浏览器中生成相同的标记: */ var xsltProcessor = new XSLTProcessor(); var xslStylesheet; var xmlDoc; // 异步获取一个XSL文件 var requestXsl = new XMLHttpRequest(); requestXsl.onreadystatechange = function (request) { xslStylesheet = request.responseXML; }; requestXsl.open('GET', 'example.xml', true); requestXsl.send(null); // 异步取得一个XML文件 var requestXML = new XMLHttpRequest(); requestXML.onreadystatechange = function (request) { xmlDoc = request.responseXML; }; requestXML.open('GET', 'example.xml', true); requestXML.send(null); var processor = function () { if (xslStylesheet && xmlDoc) { clearInterval(this); // 通过XSLT转换XML xsltProcessor.importStylesheet(xslStylesheet); var fragment = xsltProcessor.transformToFragment(xmlDoc, document); ADS.$('example').appendChild(fragment); } }; // 每200毫秒检查一次文件是否载入完成 setInterval(processor, 200); // 对XML响应使用DOM2核心方法 var messages = request.responseXML.getElementsByTagName('message'); for (var i = 0; i < messages.length; i++) { ADS.$('example').appendChild(messages[i]); } /* 不过,XML也有一些不足的地方。通常,围绕数据的XML标记都将是响应中的大部分内容,而在高通信流量的情况下使用体积更小的方法会提高效率。而且,再夸浏览器的环境中处理XSLT也是有问题的。 */ // 其他代替方案 /** * 1. 纯文本 * * 对于确实简单明了的请求,可以只返回未经格式化或任何特殊处理的纯文本字符串。而发送纯文本作为响应的问题是缺少元数据。如果是在一个句子中返回信息,那么没有任何方法能够指示信息发送成功还是出现了错误。但是,如果只需要取得表示是或否的响应,则可以响应true或false,甚至只返回t或f,然后再据此采取措施: */ request.onreadystatechange = function () { if (request.readyState === 4 && request.status === 200) { if (request.responseText === 't') { // 服务器处理成功 } else { // 服务器处理失败 } } }; /** * 2. HTML * * 另一个常见的选择是常规的HTML代码作为响应: * <p class="success">The response was successful</p> * 这样就具备了通过元素属性添加元数据的能力,而且还能使用innerHTML属性将responseText中的代码插入到文档中: */ request.onreadystatechange = function () { if (request.readyState === 4 && request.status === 200) { ADS.$('example').innerHTML = request.responseText; } }; /* 在实践中,这样做并不是一个好习惯 1.与XML相似。响应可能会因为标记而变得无谓的臃肿,而那么标记都可以用过其他方式提取并放到可重用的js对象中。 2.作为一个字符串,HTML不能为脚本提供指向其内部元素的任何直接关联。必须在用过innerHTML属性将HTML插入到文档后,使用document.getElementById()或类似方法创建相应的关联。 3.以HTML作为响应不能使应用程序中的各个层之间实现很好的分离。因为不能把服务器或脚本的功能重用于多个实例,除非这些实例都使用同样的标记和方法。而保持标记尽可能多的本地化并且只在响应中发送必要的信息,然后再通过DOM方法将这些信息转换为适当的结构则是更好的方式。 4.在IE中,innerHTML属性在处理表格和选择列表时也存在问题。 如果响应的是XHTML并且具有适当的Content-Type信息,则responseXML中会生成该HTML的一个DOM文档表示。不过,仅适用与DOM2核心(而不是DOM2 HTML),也就是说对responseXML中的DOM文档无法使用HTML元素中的许多属性。 */ /** * 3. Javascript代码 * * 作为另外一个选项,可以在响应中传输任何js代码。可以通过在responseText属性上调用eva()方法来执行相应的代码: * * 使用js也会导致一些问题。首先,使用eval()被认为是一种坏习惯,因为一不小心就可能造成安全隐患。其次,与使用HTML一样的另一个原因,即js将因此分散到应用程序不同部分的一些文件中。 */ request.onreadystatechange = function () { if (request.readyState === 4 && request.status === 200) { eval(request.responseText); } }; /** * 4. JSON * * 最后一个替代方案是只使用JSON(js对象表示法)。本质上说,JSON语法是js对象字面量表示法的一个子集。 * { message: 'The response was successful', type: 'success' } * 随后可以使用eval()来将JSON解析为本地js对象,从该对象中再取得任何想要的信息: */ request.onreadystatechange = function () { if (request.readyState === 4 && request.status === 200) { // 对JSON求值以生成response对象 var response = eval('(' + request.responseText + ')'); // 通过innerHTML对response执行某些操作 ADS.$('example').innerHTML = '<p class=">' + response.type + '">' + response.message + '</p>'; // 或者直接对用户给出提示 alert(response.message); // 或使用DOM方法 var p = document.createElement('p'); p.className = response.type; p.appendChild(document.createTextNode(response.message)); ADS.$('example').appendChild(p); } }; /* 使用eval()解析JSON虽然快捷而简单,但同样也存在不安全的问题。最好的办法就是使用一种只识别有效JSON语法的解析程序,即将JSON的清晰简单与更加安全的获取方法结合起来。 */ /** * 检查JSON文本,确保安全 * @param {String} s JSON字符串 * @param {Function} filter 过滤方法 * @return {*} */ function parseJSON(s, filter) { var j; /** * 递归地遍历了新生成的结构 而且将每个名/值对传递给一个过滤函数,以便进行 可能的转换 * @param k * @param v * @return {*} */ function walk(k, v) { if (v && typeof v === 'object') { for (var i in v) { if (v.hasOwnProperty(i)) { v[i] = walk(i, v[i]); } } } return filter(k, v); } /* 解析通过3个阶段进行。第一阶段,通过正则表达式 检测JSON文本,查找非JSON字符串。其中,特别关注 “()”和"new",因为它们会引起语句的调用,还有“=”, 因为它会导致变量的值发生改变。不过,为安全起见 这里会拒绝所有不希望出现的字符串 */ /* 首先这个串分成两部分,看中间的或符号(|) "(\\.|[^\\\n\r])*?"和[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t] 先分解"(\\.|[^\\\n\r])*?" 它匹配一个双引号的字符串,两边引号不说了括号内一个“|”又分成两段 “\\.“匹配一个转义字符 比如js字符串里的\n,\r,\',\"等。[^\\\n\r]匹配一个非\,回车换行的字符 其实它就是js里字符串的规则---不包含回车换行,回车换行用 \n\r表示,\后面跟一个字符表示转义 其次看[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t] 它匹配一个单个字符,这个字符可以是 ,,:,{,},[,],数字,除 "\n" 之外的任何单个字符,-,+,E,a,e,f,l,n,r-u之间的字符,回车,换行,制表符, */ if (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(s)) { /* 第二阶段,使用eval()函数将JSON文本编译为js 结构。其中的“{”操作符具有语法上的二义性,即它可 以定义一个语句块,也可以表示对象字面量。这里将 JSON文本用括号括起来是为了消除这种二义性 */ try { j = eval('(' + s + ')'); } catch (e) { throw new SyntaxError('parseJSON'); } } else { throw new SyntaxError('parseJSON'); } /* 在可选的第三阶段,代码递归地遍历了新生成的结构 而且将每个名/值对传递给一个过滤函数,以便进行 可能的转换 */ if (typeof filter === 'function') { j = walk('', j); } return j; } /** * 设置XMLHttpRequest对象的各个不同的部分 * @param url * @param options * 参数options的成员属性 * method, 适用于请求的方法,默认为GET。 * send, 是一个包含在XMLHttpRequest.send()中的可选字符串,默认为null。 * loadListener, 是当readyState为1时调用的onreadystatechange侦听器 * loadedListener, 是当readyState为2时调用的onreadystatechange侦听器。 * interactiveListener, 是当readyState为3时调用的onreadystatechange侦听器。 * jsResponseListener, 是当请求成功并且响应的Content-Type为application/javascript或text/javascript时调用的侦听器,这个侦听器将从响应中取得js字符串作为其第一个参数。如果要执行该js字符串,必须使用eval()方法。 * jsonResponseListener, 是当请求成功并且响应的Content-Type为application/json时调用的侦听器。这个侦听器将从响应中取得JSON对象作为其第一个参数。 * xmlResponseListener, 是当请求成功并且响应的Content-Type为application/xml或application/xhtml+xml时调用的侦听器。这个侦听器将从响应中取得的XML DOM文档作为其第一个参数。 * htmlResponseListener, 是当请求成功并且响应的content-Type为text/html时调用的侦听器。这个侦听器将从响应中取得的HTML字符串作为其第一个参数。 * completeListener, 是当上面所列的针对Content-Type的响应侦听器调用之后被调用的侦听器。这个方法总是在成功的响应最后被调用,也就是说如果相应中没有适当的Content-Type头部信息,那么你可以制定这个方法作为兜底儿的侦听器。 * errorListener, 是当响应状态值不是200也不是0时被调用的侦听器。如果是在不会提供适当响应代码的系统上运行(如硬盘驱动器中的本地文件系统)XMLHttpRequest,那么状态值将始终为0.在这种情况下,只有completeListener会被调用。 */ /* demo: ADS.ajaxRequest('/path/to/script/',{ method:'GET', completeListener:function(){ alert(this.responseText); } }); */ // 因为使用了call和apply方法,此时的this引用的是 // 请求对象而不是onreadystatechange方法。 function getRequestObject(url, options) { // 初始化请求对象 var req = false; if (window.XMLHttpRequest) { req = new window.XMLHttpRequest(); } else if (window.ActiveXObject) { req = new ActiveXObject('Microsoft.XMLHTTP'); } if (!req) { return false; } // 定义默认的选项 options = options || {}; options.method = options.method || 'GET'; options.send = options.send || null; // 为请求的每个阶段定义不同的侦听器 req.onreadystatechange = function () { switch (req.readyState) { case 1: // 载入中 if (options.loadListener) { options.loadListener.apply(req, arguments); } break; case 2: // 载入完成 if (options.loadedListener) { options.loadedListener.apply(req, arguments); } break; case 3: // 交互 if (options.interactiveListener) { options.interactiveListener.apply(req, arguments); } break; case 4: // 完成 // 如果失败则抛出错误 try { if (req.status && req.status === 200) { // 针对Content-type的特殊侦听器 // 由于Content-Type头部中可能包含字符集,如: // Content-Type: text/html; charset=ISO-8859-4 // 因此通过正则表达式提取出所需的部分 var contentType = req.getResponseHeader('Content-Type'); var mimeType = contentType.match(/\s*([^;]+)\s*(;|$)/i)[1]; switch (mimeType) { case 'text/javascript': case 'application/javascript': // 响应时javascript,因此以 // req.responseText作为回调函数 if (options.jsResponseListener) { options.jsResponseListener.call(req, req.responseText); } break; case 'application/json': // 响应是JSON,因此需要用匿名函数对 // req.responseText进行解析 // 已返回作为回调参数的JSON对象 if (options.jsonResponseListener) { var json; try { json = parseJSON(req.responseText); } catch (e) { json = false; } options.jsonResponseListener.call(req, json); } break; case 'text/xml': case 'application/xml': case 'application/xhtml+xml': // 响应是XML,因此以 // req.responseXML作为 // 回调的参数 // 此时是Document对象 if (options.xmlResponseListener) { options.xmlResponseListener.call(req, req.responseText); } break; case 'text/html': // 响应是HTML,因此以 // req.responseText作为 // 回调的参数 if (options.htmlResponseListener) { options.htmlResponseListener.call(req, req.responseText); } break; default: break; } // 针对响应成功完成的侦听器 if (options.completeListener) { options.completeListener.apply(req, arguments); } } else { // 相应完成但却存在错误 if (options.errorListener) { options.errorListener.apply(req, arguments); } } } catch (e) { // 忽略错误 } break; } }; // 开启请求 req.open(options.method, url, true); // 添加特殊的头部信息以标识请求 req.setRequestHeader('X-ADS-Ajax-Request', 'AjaxRequest'); return req; } window.ADS.getRequestObject = getRequestObject; // 通过简单的包装getRequestObject()和send() // 方法发送XMLHttpRequest对象的请求 function ajaxRequest(url, options) { var req = getRequestObject(url, options); return req.send(options.send); } window.ADS.ajaxRequest = ajaxRequest;