总体概述
jasmine-ajax主要五个对象组成:
- 请求伪造对象(FackXMLHttpRequest):
function FakeXMLHttpRequest() {}
- 请求跟踪对象(RequestTracker):
function RequestTracker() {}
- 请求拦截对象(
RequestStub
):用作对伪造的AJAX请求做出响应。
function RequestStub(url,stubData,method) {}
- 请求拦截跟踪对象(StubTracker):
function StubTracker() {}
- 参数解析(ParamParser):
function ParamParser() {}
辅助函数
arrayContains
function arrayContains(arr,item) { for (var i = 0; i < arr.length; i++) { if (arr[i] === item) { return true; } } return false; }
函数结构很明显,判定一个数组是否包含某一个值。
extend
function extend(destination,source,propertiesToSkip) { propertiesToSkip = propertiesToSkip || []; for (var property in source) { if (!arrayContains(propertiesToSkip,property)) { destination[property] = source[property]; } } return destination; }
此函数实现对象扩展,propertiesToSkip
参数为source
中需排除掉的键,余下的键值对覆盖写入destination
。
初始化语句
if (typeof window === "undefined" && typeof exports === "object") { exports.MockAjax = MockAjax; jasmine.Ajax = new MockAjax(exports); } else { window.MockAjax = MockAjax; jasmine.Ajax = new MockAjax(window); }
首先判定是否commonjs
环境,不是则传递window
变量。在describe
内部,即可访问通过jasmine.Ajax
使用MockAjax对象。
RequestTracker
其实结构非常明显,使用jasmine.Ajax.requests即可访问到RequestTracker对象,然后通过first,mostRecent,at,filter 等方法获取仿造的request对象,多用于对HTTP请求对象进行判定,如方法,路径,数据,headers等等,需要重点关注。reset,track手动调用意义不大,前者用于重置变量环境,后者在生成伪造XHR时调用。
function RequestTracker() { var requests = []; this.track = function(request) { requests.push(request); }; this.first = function() { return requests[0]; }; this.count = function() { return requests.length; }; this.reset = function() { requests = []; }; this.mostRecent = function() { return requests[requests.length - 1]; }; this.at = function(index) { return requests[index]; }; this.filter = function(url_to_match) { if (requests.length === 0) { return []; } var matching_requests = []; for (var i = 0; i < requests.length; i++) { if (url_to_match instanceof RegExp && url_to_match.test(requests[i].url)) { matching_requests.push(requests[i]); } else if (url_to_match instanceof Function && url_to_match(requests[i])) { matching_requests.push(requests[i]); } else { if (requests[i].url === url_to_match) { matching_requests.push(requests[i]); } } } return matching_requests; }; }
StubTracker
通过jasmine.Ajax.stubs
即可访问到StubTracker
对象,主动调用意义不大。reset
方法用于变量环境重置,addStub
方法在生成RequestStub
对象时调用。findStub
在xhr.open()
调用时,判定是否定义过response
。
function StubTracker() { var stubs = []; this.addStub = function(stub) { stubs.push(stub); }; this.reset = function() { stubs = []; }; this.findStub = function(url,data,method) { for (var i = stubs.length - 1; i >= 0; i--) { var stub = stubs[i]; if (stub.matches(url,method)) { return stub; } } }; }
RequestStub
请求拦截在url,method同时匹配的情况下才会启动拦截。
function RequestStub(url,method) { var normalizeQuery = function(query) { return query ? query.split('&').sort().join('&') : undefined; }; if (url instanceof RegExp) { this.url = url; this.query = undefined; } else { var split = url.split('?'); this.url = split[0]; this.query = split.length > 1 ? normalizeQuery(split[1]) : undefined; } this.data = normalizeQuery(stubData); this.method = method; this.andReturn = function(options) { this.status = options.status || 200; this.contentType = options.contentType; this.responseText = options.responseText; }; this.matches = function(fullUrl,method) { var matches = false; fullUrl = fullUrl.toString(); if (this.url instanceof RegExp) { matches = this.url.test(fullUrl); } else { var urlSplit = fullUrl.split('?'),url = urlSplit[0],query = urlSplit[1]; matches = this.url === url && this.query === normalizeQuery(query); } return matches && (!this.data || this.data === normalizeQuery(data)) && (!this.method || this.method === method); }; }
ParamParser
主要负责值的解析。paramParsers参数为parser对象数组。findParser遍历paramParsers,通过test属性判定是否启用对应的parse方法处理数据。若决定调用,则遍历结束,返回该对象。add方法一般会通过jasmine.Ajax.addCustomParamParser(parser)调用,优先级上,后添加的>先添加的>默认的。
function ParamParser() { var defaults = [ { test: function(xhr) { return (/^application\/json/).test(xhr.contentType()); },parse: function jsonParser(paramString) { return JSON.parse(paramString); } },{ test: function(xhr) { return true; },parse: function naiveParser(paramString) { var data = {}; var params = paramString.split('&'); for (var i = 0; i < params.length; ++i) { var kv = params[i].replace(/\+/g,' ').split('='); var key = decodeURIComponent(kv[0]); data[key] = data[key] || []; data[key].push(decodeURIComponent(kv[1])); } return data; } } ]; var paramParsers = []; this.add = function(parser) { paramParsers.unshift(parser); }; this.findParser = function(xhr) { for(var i in paramParsers) { var parser = paramParsers[i]; if (parser.test(xhr)) { return parser; } } }; this.reset = function() { paramParsers = []; for(var i in defaults) { paramParsers.push(defaults[i]); } }; this.reset(); }
FackXMLHttpRequest
代码过长,分段说明。
- FakeXMLHttpRequest
实例化后,即添加进入requestTracker,便于后期访问。
function FakeXMLHttpRequest() { requestTracker.track(this); this.requestHeaders = {}; this.overriddenMimeType = null; }
- 原型继承
此处感觉比较好玩,是继承真的XMLHttpRequest对象,只是去掉几个特殊键值对,后面会进行处理。
var iePropertiesThatCannotBecopied = ['responseBody','responseText','responseXML','status','statusText','responseTimeout']; extend(FakeXMLHttpRequest.prototype,new window.XMLHttpRequest(),iePropertiesThatCannotBecopied);
- Request伪造
熟悉原生ajax的应该很容易看懂。重点在于,send方法调用时,会立即在stubs里寻找匹配当前url,method,data的响应拦截器,如果预定义过了,则会立刻传递给response函数进行响应,否则需要之后手动调用response函数进行响应。
extend(FakeXMLHttpRequest.prototype,{ open: function() { this.method = arguments[0]; this.url = arguments[1]; this.username = arguments[3]; this.password = arguments[4]; this.readyState = 1; this.onreadystatechange(); },setRequestHeader: function(header,value) { if(this.requestHeaders.hasOwnProperty(header)) { this.requestHeaders[header] = [this.requestHeaders[header],value].join(','); } else { this.requestHeaders[header] = value; } },overrideMimeType: function(mime) { this.overriddenMimeType = mime; },abort: function() { this.readyState = 0; this.status = 0; this.statusText = "abort"; this.onreadystatechange(); },readyState: 0,onload: function() { },onreadystatechange: function(isTimeout) { },status: null,send: function(data) { this.params = data; this.readyState = 2; this.onreadystatechange(); var stub = stubTracker.findStub(this.url,this.method); if (stub) { this.response(stub); } },contentType: function() { return findHeader('content-type',this.requestHeaders); },data: function() { if (!this.params) { return {}; } return paramParser.findParser(this).parse(this.params); },getResponseHeader: function(name) { return findHeader(name,this.responseHeaders); },getAllResponseHeaders: function() { var responseHeaders = []; for (var i in this.responseHeaders) { if (this.responseHeaders.hasOwnProperty(i)) { responseHeaders.push(i + ': ' + this.responseHeaders[i]); } } return responseHeaders.join('\r\n'); },responseText: null,response: function(response) { if (this.readyState === 4) { throw new Error("FakeXMLHttpRequest already completed"); } this.status = response.status; this.statusText = response.statusText || ""; this.responseText = response.responseText || ""; this.readyState = 4; this.responseHeaders = response.responseHeaders || {"Content-Type": response.contentType || "application/json" }; this.onload(); this.onreadystatechange(); },responseTimeout: function() { if (this.readyState === 4) { throw new Error("FakeXMLHttpRequest already completed"); } this.readyState = 4; jasmine.clock().tick(30000); this.onreadystatechange('timeout'); } }); return FakeXMLHttpRequest; }
经验交流
QQ: 491229492
Email: huang.jian@eisoo.com