/** * ER (Enterprise RIA) * Copyright 2013 Baidu Inc. All rights reserved. * * @ignore * @file AJAX相关方法 * @author otakustay */ define( function (require) { var util = require('./util'); var TIMESTAMP_PARAM_KEY = '_'; function serializeArray(prefix, array) { var encodedKey = prefix ? encodeURIComponent(prefix) : ''; var encoded = []; for (var i = 0; i < array.length; i++) { var item = array[i]; encoded[i] = this.serializeData('', item); } return encodedKey ? encodedKey + '=' + encoded.join(',') : encoded.join(','); } function serializeData(prefix, data) { if (arguments.length === 1) { data = prefix; prefix = ''; } if (data == null) { data = ''; } var getKey = this.serializeData.getKey; var encodedKey = prefix ? encodeURIComponent(prefix) : ''; var type = Object.prototype.toString.call(data); switch (type) { case '[object Array]': return this.serializeArray(prefix, data); case '[object Object]': var result = []; for (var name in data) { var propertyKey = getKey(name, prefix); var propertyValue = this.serializeData(propertyKey, data[name]); result.push(propertyValue); } return result.join('&'); default: return encodedKey ? encodedKey + '=' + encodeURIComponent(data) : encodeURIComponent(data); } } serializeData.getKey = function (propertyName, parentKey) { return parentKey ? parentKey + '.' + propertyName : propertyName; }; /** * Ajax类 * * 通过`require('er/ajax').Ajax`访问该类构造函数,其中`require('er/ajax')`是该类的全局实例 * * @extends mini-event.EventTarget * @constructor */ function Ajax() { /** * AJAX钩子 * * @type {meta.AjaxHook} */ this.hooks = { serializeData: serializeData, serializeArray: serializeArray }; /** * AJAX配置 * * @type {meta.AjaxOption} */ this.config = { cache: false, timeout: 0, charset: '' }; } util.inherits(Ajax, require('mini-event/EventTarget')); /** * 发起`XMLHttpRequest`请求 * * @param {meta.AjaxOption} options 相关配置 * @return {meta.FakeXHR} */ Ajax.prototype.request = function (options) { if (typeof this.hooks.beforeExecute === 'function') { this.hooks.beforeExecute(options); } var assert = require('./assert'); assert.hasProperty(options, 'url', 'url property is required'); var defaults = { method: 'POST', data: {}, cache: this.config.cache, timeout: this.config.timeout, charset: this.config.charset }; var util = require('./util'); options = util.mix(defaults, options); var Deferred = require('./Deferred'); var requesting = new Deferred(); if (typeof this.hooks.beforeCreate === 'function') { var canceled = this.hooks.beforeCreate(options, requesting); if (canceled === true) { var fakeXHR = requesting.promise; fakeXHR.abort = function () {}; fakeXHR.setRequestHeader = function () {}; return fakeXHR; } } var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP'); var fakeXHR = requesting.promise; var xhrWrapper = { abort: function () { // 有些浏览器`abort()`就会把`readyState`变成4, // 这就会导致进入处理函数变成**resolved**状态, // 因此事先去掉处理函数,然后直接进入**rejected**状态 xhr.onreadystatechange = null; try { xhr.abort(); } catch (ex) { } if (!fakeXHR.status) { fakeXHR.status = 0; } fakeXHR.readyState = xhr.readyState; fakeXHR.responseText = ''; fakeXHR.responseXML = ''; requesting.reject(fakeXHR); }, setRequestHeader: function (name, value) { xhr.setRequestHeader(name, value); }, getAllResponseHeaders: function () { return xhr.getAllResponseHeaders(); }, getResponseHeader: function (name) { return xhr.getResponseHeader(name); } }; util.mix(fakeXHR, xhrWrapper); fakeXHR.then( function () { /** * @event done * * 任意一个请求成功时触发 * * @param {meta.AjaxOption} options 请求的配置信息 * @param {meta.FakeXHR} xhr 请求对象 */ this.fire( 'done', { xhr: fakeXHR, options: options } ); }, function () { /** * @event fail * * 任意一个请求失败时触发 * * @param {meta.FakeXHR} xhr 请求对象 * @param {meta.AjaxOption} options 请求的配置信息 */ this.fire( 'fail', { xhr: fakeXHR, options: options } ); } ); var processRequestStatus = function () { if (xhr.readyState === 4) { var status = fakeXHR.status || xhr.status; // IE9会把204状态码变成1223 if (status === 1223) { status = 204; } fakeXHR.status = fakeXHR.status || status; fakeXHR.readyState = xhr.readyState; fakeXHR.responseText = xhr.responseText; fakeXHR.responseXML = xhr.responseXML; if (typeof this.hooks.afterReceive === 'function') { this.hooks.afterReceive(fakeXHR, options); } // 如果请求不成功,也就不用再分解数据了,直接丢回去就好 if (status < 200 || (status >= 300 && status !== 304)) { requesting.reject(fakeXHR); return; } var data = xhr.responseText; if (options.dataType === 'json') { try { data = util.parseJSON(data); } catch (ex) { // 服务器返回的数据不符合JSON格式,认为请求失败 fakeXHR.error = ex; requesting.reject(fakeXHR); return; } } if (typeof this.hooks.afterParse === 'function') { try { data = this.hooks.afterParse(data, fakeXHR, options); } catch (ex) { fakeXHR.error = ex; requesting.reject(fakeXHR); return; } } // 数据处理成功后,进行回调 requesting.resolve(data); } }; xhr.onreadystatechange = util.bind(processRequestStatus, this); var method = options.method.toUpperCase(); var data = {}; if (method === 'GET') { util.mix(data, options.data); } if (options.cache === false) { data[TIMESTAMP_PARAM_KEY] = +new Date(); } var query = this.hooks.serializeData('', data, 'application/x-www-form-urlencoded'); var url = options.url; if (query) { var delimiter = url.indexOf('?') >= 0 ? '&' : '?'; url += delimiter + query; } xhr.open(method, url, true); if (typeof this.hooks.beforeSend === 'function') { this.hooks.beforeSend(fakeXHR, options); } if (method === 'GET') { xhr.send(); } else { var contentType = options.contentType || 'application/x-www-form-urlencoded'; var query = this.hooks.serializeData('', options.data, contentType, fakeXHR); if (options.charset) { contentType += ';charset=' + options.charset; } xhr.setRequestHeader('Content-Type', contentType); xhr.send(query); } if (options.timeout > 0) { var notifyTimeout = function () { /** * @event timeout * * 任意一个请求成功时触发, * 在此事件后会再触发一次{@link Ajax#fail}事件 * * @param {meta.FakeXHR} xhr 请求对象 * @param {meta.AjaxOption} options 请求的配置信息 */ this.fire( 'timeout', { xhr: fakeXHR, options: options } ); fakeXHR.status = 408; // HTTP 408: Request Timeout fakeXHR.abort(); }; var tick = setTimeout(util.bind(notifyTimeout, this), options.timeout); fakeXHR.ensure(function () { clearTimeout(tick); }); } return fakeXHR; }; /** * 发起一个`GET`请求 * * @param {string} url 请求的地址 * @param {Object} [data] 请求的数据 * @param {boolean} [cache] 决定是否允许缓存 * @return {meta.FakeXHR} */ Ajax.prototype.get = function (url, data, cache) { var options = { method: 'GET', url: url, data: data, cache: cache || this.config.cache }; return this.request(options); }; /** * 发起一个`GET`请求并获取JSON数据 * * @param {string} url 请求的地址 * @param {Object} [data] 请求的数据 * @param {boolean} [cache] 决定是否允许缓存 * @return {meta.FakeXHR} */ Ajax.prototype.getJSON = function (url, data, cache) { var options = { method: 'GET', url: url, data: data, dataType: 'json', cache: cache || this.config.cache }; return this.request(options); }; /** * 发起一个`POST`请求 * * @param {string} url 请求的地址 * @param {Object} [data] 请求的数据 * @param {string} [dataType="json"] 指定响应的数据格式 * @return {meta.FakeXHR} */ Ajax.prototype.post = function (url, data, dataType) { var options = { method: 'POST', url: url, data: data, dataType: dataType || 'json' }; return this.request(options); }; /** * 发送一个日志请求,该请求只负责发出,不负责保证送达,且不支持回调函数 * * @param {string} url 发送的目标URL * @param {Object} [data] 额外添加的参数 */ Ajax.prototype.log = function (url, data) { var img = new Image(); var pool = window.ER_LOG_POOL || (window.ER_LOG_POOL = {}); var id = +new Date(); pool[id] = img; img.onload = img.onerror = img.onabort = function () { // 如果这个img很不幸正好加载了一个存在的资源,又是个gif动画, // 则在gif动画播放过程中,img会多次触发onload,因此一定要清空 img.onload = img.onerror = img.onabort = null; pool[id] = null; // 下面这句非常重要, // new Image创建的是DOM, // DOM的事件中形成闭包环引用DOM是典型的内存泄露, // 因此这里一定要置为null img = null; }; var query = this.hooks.serializeData('', data, 'application/x-www-form-urlencoded'); if (query) { var delimiter = url.indexOf('?') >= 0 ? ':' : '?'; url += delimiter + query; } // 一定要在注册了事件之后再设置src, // 不然如果图片是读缓存的话,会错过事件处理, // 最后,对于url最好是添加客户端时间来防止缓存, // 同时服务器也配合一下传递`Cache-Control: no-cache;` img.src = url; }; var instance = new Ajax(); instance.Ajax = Ajax; return instance; } );