657 lines
21 KiB
JavaScript
657 lines
21 KiB
JavaScript
/**
|
||
* Request组件, modern版, 支持IE9+,chrome,FF
|
||
* @authors yutent (yutent@doui.cc)
|
||
* @date 2016-11-27 13:08:40
|
||
*
|
||
*/
|
||
|
||
'use strict';
|
||
define(['yua', './promise'], function(yua) {
|
||
var _request = function(url, protocol) {
|
||
this.transport = true;
|
||
protocol = (protocol + '').trim().toUpperCase();
|
||
this.xhr = Xhr();
|
||
this.defer = Promise.defer();
|
||
this.opt = {
|
||
url: (url + '').trim(),
|
||
type: protocol || 'GET',
|
||
form: '',
|
||
data: {},
|
||
headers: {},
|
||
timeoutID: 0,
|
||
uuid: Math.random()
|
||
.toString(16)
|
||
.substr(2)
|
||
};
|
||
},
|
||
_requestp = _request.prototype,
|
||
toS = Object.prototype.toString,
|
||
win = window,
|
||
doc = win.document,
|
||
encode = encodeURIComponent,
|
||
decode = decodeURIComponent,
|
||
noop = function(e, res) {
|
||
this.defer.resolve(res);
|
||
};
|
||
|
||
// -----------------------------
|
||
|
||
// 本地协议判断正则
|
||
var rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/;
|
||
var isLocal = false;
|
||
try {
|
||
isLocal = rlocalProtocol.test(location.ptyperotocol);
|
||
} catch (e) {}
|
||
|
||
var rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/gm;
|
||
|
||
// ----------------- 一些兼容性预处理 --------------------
|
||
|
||
win.Xhr = function() {
|
||
return new XMLHttpRequest();
|
||
};
|
||
var supportCors = 'withCredentials' in Xhr();
|
||
|
||
// ------------------- 几个解释方法 -----------------------
|
||
|
||
function serialize(p, obj, q) {
|
||
var k;
|
||
if (Array.isArray(obj)) {
|
||
obj.forEach(function(it, i) {
|
||
k = p ? p + '[' + (Array.isArray(it) ? i : '') + ']' : i;
|
||
if (typeof it === 'object') {
|
||
serialize(k, it, q);
|
||
} else {
|
||
q(k, it);
|
||
}
|
||
});
|
||
} else {
|
||
for (var i in obj) {
|
||
k = p ? p + '[' + i + ']' : i;
|
||
if (typeof obj[i] === 'object') {
|
||
serialize(k, obj[i], q);
|
||
} else {
|
||
q(k, obj[i]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var Format = function() {};
|
||
|
||
Format.prototype = {
|
||
parseJS: function(code) {
|
||
code = (code + '').trim();
|
||
if (code) {
|
||
if (code.indexOf('use strict') === 1) {
|
||
var script = doc.createElement('script');
|
||
script.text = code;
|
||
doc.head.appendChild(script).parentNode.removeChild(script);
|
||
} else {
|
||
eval(code);
|
||
}
|
||
}
|
||
},
|
||
parseXML: function(data, xml, tmp) {
|
||
try {
|
||
tmp = new DOMParser();
|
||
xml = tmp.parseFromString(data, 'text/xml');
|
||
} catch (e) {
|
||
xml = void 0;
|
||
}
|
||
|
||
if (
|
||
!xml ||
|
||
!xml.documentElement ||
|
||
xml.getElementsByTagName('parsererror').length
|
||
) {
|
||
console.error('Invalid XML: ' + data);
|
||
}
|
||
return xml;
|
||
},
|
||
parseHTML: function(html) {
|
||
return yua.parseHTML(html);
|
||
},
|
||
param: function(obj) {
|
||
if (!obj || typeof obj === 'string' || typeof obj === 'number')
|
||
return obj;
|
||
|
||
var arr = [];
|
||
var q = function(k, v) {
|
||
if (/native code/.test(v)) return;
|
||
|
||
v = typeof v === 'function' ? v() : v;
|
||
v = toS.call(v) !== '[object File]' ? encode(v) : v;
|
||
|
||
arr.push(encode(k) + '=' + v);
|
||
};
|
||
|
||
if (typeof obj === 'object') serialize('', obj, q);
|
||
|
||
return arr.join('&');
|
||
},
|
||
parseForm: function(form) {
|
||
var data = {};
|
||
for (var i = 0, field; (field = form.elements[i++]); ) {
|
||
switch (field.type) {
|
||
case 'select-one':
|
||
case 'select-multiple':
|
||
if (field.name.length && !field.disabled) {
|
||
for (var j = 0, opt; (opt = field.options[j++]); ) {
|
||
if (opt.selected) {
|
||
data[field.name] = opt.value || opt.text;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case 'file':
|
||
if (field.name.length && !field.disabled) {
|
||
data[field.name] = field.files[0];
|
||
}
|
||
break;
|
||
case undefined:
|
||
case 'submit':
|
||
case 'reset':
|
||
case 'button':
|
||
break; //按钮啥的, 直接忽略
|
||
case 'radio':
|
||
case 'checkbox':
|
||
// 只处理选中的
|
||
if (!field.checked) break;
|
||
default:
|
||
if (field.name.length && !field.disabled) {
|
||
data[field.name] = field.value;
|
||
}
|
||
}
|
||
}
|
||
return data;
|
||
},
|
||
merge: function(a, b) {
|
||
if (typeof a !== 'object' || typeof b !== 'object')
|
||
throw new TypeError('argument must be an object');
|
||
|
||
if (Object.assign) return Object.assign(a, b);
|
||
|
||
for (var i in b) {
|
||
a[i] = b[i];
|
||
}
|
||
return a;
|
||
}
|
||
};
|
||
|
||
var F = new Format();
|
||
|
||
// ---------------------------------------------------------
|
||
// -------------------- request 模块开始 --------------------
|
||
// ---------------------------------------------------------
|
||
|
||
var requestConvert = {
|
||
text: function(val) {
|
||
return val;
|
||
},
|
||
xml: function(val, xml) {
|
||
return xml !== undefined ? xml : F.parseXML(val);
|
||
},
|
||
html: function(val) {
|
||
return F.parseHTML(val);
|
||
},
|
||
json: function(val) {
|
||
return JSON.parse(val);
|
||
},
|
||
script: function(val) {
|
||
return F.parseJS(val);
|
||
},
|
||
jsonp: function(name) {
|
||
var json = request.cache[name];
|
||
delete request.cache[name];
|
||
return json;
|
||
}
|
||
};
|
||
var requestExtend = {
|
||
formData: function() {
|
||
if (this.opt.form) {
|
||
var data = F.parseForm(this.opt.form);
|
||
F.merge(this.opt.data, data);
|
||
}
|
||
|
||
var form = new FormData();
|
||
for (var i in this.opt.data) {
|
||
var el = this.opt.data[i];
|
||
if (Array.isArray(el)) {
|
||
el.forEach(function(it) {
|
||
form.append(i + '[]', it);
|
||
});
|
||
} else {
|
||
form.append(i, this.opt.data[i]);
|
||
}
|
||
}
|
||
return form;
|
||
},
|
||
jsonp: function(jsonpcallback) {
|
||
win[jsonpcallback] = function(val) {
|
||
delete win[jsonpcallback];
|
||
request.cache[jsonpcallback] = val;
|
||
};
|
||
},
|
||
dispatch: function(self) {
|
||
if (!this.transport) return this.defer.reject('Request pending...');
|
||
|
||
var _this = this,
|
||
result = {
|
||
response: {
|
||
url: this.opt.url,
|
||
headers: { 'content-type': '' }
|
||
},
|
||
request: {
|
||
url: this.opt.url,
|
||
headers: _this.opt.headers
|
||
},
|
||
status: self === null ? 504 : 200,
|
||
statusText: self === null ? 'Connected timeout' : 'ok',
|
||
text: '',
|
||
body: '',
|
||
error: null
|
||
};
|
||
|
||
//状态为4,既已成功, 则清除超时
|
||
clearTimeout(_this.opt.timeoutID);
|
||
|
||
if (
|
||
typeof this.transport === 'object' &&
|
||
this.opt.type === 'JSONP'
|
||
) {
|
||
//移除script
|
||
// this.transport.parentNode.removeChild(this.transport);
|
||
|
||
//超时返回
|
||
if (self !== null) {
|
||
var exec =
|
||
!this.transport.readyState ||
|
||
this.transport.readyState === 'loaded' ||
|
||
this.transport.readyState === 'complete';
|
||
|
||
if (exec) {
|
||
result.body = requestConvert.jsonp(
|
||
this.opt.data.callback
|
||
);
|
||
result.text = JSON.stringify(result.body);
|
||
}
|
||
}
|
||
|
||
this.callback(result.error, result);
|
||
} else {
|
||
//成功的回调
|
||
var isSucc = self
|
||
? (self.status >= 200 && self.status < 300) ||
|
||
self.status === 304
|
||
: false,
|
||
headers =
|
||
(self && self.getAllResponseHeaders().split('\n')) ||
|
||
[];
|
||
|
||
//处理返回的Header
|
||
headers.forEach(function(it, i) {
|
||
it = it.trim();
|
||
if (it) {
|
||
it = it.split(':');
|
||
result.response.headers[
|
||
it.shift().toLowerCase()
|
||
] = it.join(':').trim();
|
||
}
|
||
});
|
||
|
||
if (isSucc) {
|
||
result.status = self.status;
|
||
if (result.status === 204) {
|
||
result.statusText = 'no content';
|
||
} else if (result.status === 304) {
|
||
result.statusText = 'not modified';
|
||
}
|
||
} else {
|
||
result.status = self === null ? 504 : self.status || 500;
|
||
result.statusText =
|
||
self === null
|
||
? 'Connected timeout'
|
||
: self.statusText || 'Internal Server Error';
|
||
result.error = F.merge(new Error(result.statusText), {
|
||
status: result.status
|
||
});
|
||
}
|
||
|
||
try {
|
||
//处理返回的数据
|
||
var dataType = result.response.headers[
|
||
'content-type'
|
||
].match(/json|xml|script|html/i) || ['text'];
|
||
|
||
dataType = dataType[0].toLowerCase();
|
||
result.text =
|
||
(self && (self.responseText || self.responseXML)) || '';
|
||
result.body = requestConvert[dataType](
|
||
result.text,
|
||
self && self.responseXML
|
||
);
|
||
} catch (err) {
|
||
result.error = err;
|
||
result.statusText = 'parse error';
|
||
}
|
||
|
||
_this.callback(result.error, result);
|
||
}
|
||
delete _this.defer;
|
||
delete _this.transport;
|
||
delete _this.opt;
|
||
delete _this.xhr;
|
||
}
|
||
};
|
||
|
||
// 设置表单类型, 支持2种, form/json
|
||
_requestp.type = function(t) {
|
||
if (this.opt.formType === 'form-data') return this;
|
||
|
||
this.opt.formType = t || 'form';
|
||
if (t === 'form' || this.opt.type === 'GET')
|
||
this.set(
|
||
'content-type',
|
||
'application/x-www-form-urlencoded; charset=UTF-8'
|
||
);
|
||
else this.set('content-type', 'application/json; charset=UTF-8');
|
||
|
||
return this;
|
||
};
|
||
|
||
//设置头信息
|
||
_requestp.set = function(k, val) {
|
||
if (!this.transport) return;
|
||
|
||
if (typeof k === 'object') {
|
||
for (var i in k) {
|
||
i = i.toLowerCase();
|
||
this.opt.headers[i] = k[i];
|
||
}
|
||
} else if (typeof k === 'string') {
|
||
if (arguments.length < 2) throw new Error('2 arguments required');
|
||
|
||
// 全转小写,避免重复写入
|
||
k = k.toLowerCase();
|
||
|
||
if (val === undefined) delete this.opt.headers[k];
|
||
else this.opt.headers[k] = val;
|
||
} else {
|
||
throw new Error(
|
||
'arguments must be string/object, but [' + typeof k + '] given'
|
||
);
|
||
}
|
||
return this;
|
||
};
|
||
|
||
//设置请求参数
|
||
_requestp.send = function(k, val) {
|
||
if (!this.transport) return;
|
||
|
||
// 1. send方法可以多次调用, 但必须保证格式一致
|
||
// 2. 2次圴提交纯字符串也会抛出异常
|
||
if (typeof k === 'object') {
|
||
if (this.opt.data && typeof this.opt.data === 'string')
|
||
throw new Error(
|
||
'param can not be string and object at the same time'
|
||
);
|
||
if (!this.opt.data) this.opt.data = {};
|
||
|
||
F.merge(this.opt.data, k);
|
||
} else {
|
||
if (typeof k === 'string') {
|
||
if (arguments.length === 1) {
|
||
if (this.opt.data)
|
||
throw new Error('invalid param in function send');
|
||
|
||
this.opt.data = k;
|
||
} else {
|
||
if (this.opt.data && typeof this.opt.data === 'string')
|
||
throw new Error(
|
||
'param can not be string and object at the same time'
|
||
);
|
||
|
||
if (!this.opt.data) this.opt.data = {};
|
||
|
||
this.opt.data[k] = val;
|
||
}
|
||
} else {
|
||
throw new Error(
|
||
'argument of send must be string/object, but [' +
|
||
typeof k +
|
||
'] given'
|
||
);
|
||
}
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
//该方法用于 form-data类型的post请求的参数设置
|
||
_requestp.field = function(k, val) {
|
||
if (!this.transport) return this;
|
||
|
||
// 此类型优先级最高
|
||
this.opt.formType = 'form-data';
|
||
this.opt.type = 'POST';
|
||
if (
|
||
!this.opt.data ||
|
||
(this.opt.data && typeof this.opt.data !== 'object')
|
||
)
|
||
this.opt.data = {};
|
||
|
||
if (arguments.length === 1 && typeof k === 'object') {
|
||
F.merge(this.opt.data, k);
|
||
} else if (arguments.length === 2) {
|
||
this.opt.data[k] = val;
|
||
} else {
|
||
throw new TypeError(
|
||
'argument must be an object, but ' + typeof k + ' given'
|
||
);
|
||
}
|
||
return this;
|
||
};
|
||
|
||
//设置缓存
|
||
_requestp.cache = function(t) {
|
||
if (!this.transport) return;
|
||
|
||
if (this.opt.type === 'GET') this.opt.cache = !!t;
|
||
|
||
return this;
|
||
};
|
||
|
||
//取消网络请求
|
||
_requestp.abort = function() {
|
||
delete this.transport;
|
||
if (!this.opt.form) this.xhr.abort();
|
||
|
||
return this;
|
||
};
|
||
|
||
//超时设置, 单位毫秒
|
||
_requestp.timeout = function(time) {
|
||
if (typeof time !== 'number' || time < 1) return this;
|
||
|
||
this.opt.timeout = time;
|
||
return this;
|
||
};
|
||
|
||
_requestp.form = function(form) {
|
||
if (typeof form === 'object' && form.nodeName === 'FORM') {
|
||
this.opt.type = 'POST';
|
||
this.opt.form = form;
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
var originAnchor = doc.createElement('a');
|
||
originAnchor.href = location.href;
|
||
_requestp.end = function(callback) {
|
||
var _this = this;
|
||
// 回调已执行, 或已取消, 则直接返回, 防止重复执行
|
||
if (!this.transport) return this;
|
||
|
||
if (!this.opt.url) throw new Error('Invalid request url');
|
||
|
||
F.merge(this, requestExtend);
|
||
|
||
this.callback = callback || noop.bind(this);
|
||
|
||
// 1. url规范化
|
||
this.opt.url = this.opt.url
|
||
.replace(/#.*$/, '')
|
||
.replace(/^\/\//, location.protocol + '//');
|
||
|
||
// 2. 处理跨域
|
||
if (typeof this.opt.crossDomain !== 'boolean') {
|
||
var anchor = doc.createElement('a');
|
||
try {
|
||
anchor.href = this.opt.url;
|
||
// IE7及以下浏览器 '1'[0]的结果是 undefined
|
||
// IE7下需要获取绝对路径
|
||
var absUrl = !'1'[0]
|
||
? anchor.getAttribute('href', 4)
|
||
: anchor.href;
|
||
anchor.href = absUrl;
|
||
anchor.async = true;
|
||
this.opt.crossDomain =
|
||
originAnchor.protocol !== anchor.protocol ||
|
||
originAnchor.host !== anchor.host;
|
||
} catch (e) {
|
||
this.opt.crossDomain = true;
|
||
}
|
||
}
|
||
|
||
// 2.1 进一步处理跨域配置
|
||
if (this.opt.type === 'JSONP') {
|
||
//如果没有跨域,自动转回xhr GET
|
||
if (!this.opt.crossDomain) {
|
||
this.opt.type = 'GET';
|
||
} else {
|
||
this.opt.data['callback'] =
|
||
this.opt.data['callback'] || 'jsonp' + request.cid++;
|
||
this.jsonp(this.opt.data['callback']); //创建临时处理方法
|
||
}
|
||
}
|
||
// 2.2 如果不是跨域请求,则自动加上一条header信息,用以标识这是ajax请求
|
||
if (!this.opt.crossDomain) {
|
||
this.set('X-Requested-With', 'XMLHttpRequest');
|
||
} else {
|
||
supportCors && (this.xhr.withCredentials = true);
|
||
}
|
||
|
||
// 3. data转字符串
|
||
this.opt.param = F.param(this.opt.data);
|
||
|
||
// 4. 设置Content-Type类型, 默认x-www-form-urlencoded
|
||
if (!this.opt.formType) this.type('form');
|
||
|
||
// 5.处理GET请求
|
||
this.opt.hasContent = this.opt.type === 'POST'; //是否为post请求
|
||
if (!this.opt.hasContent) {
|
||
//GET请求直接把参数拼接到url上
|
||
if (this.opt.param) {
|
||
this.opt.url +=
|
||
(/\?/.test(this.opt.url) ? '&' : '?') + this.opt.param;
|
||
}
|
||
//加随机值,避免缓存
|
||
if (this.opt.cache === false)
|
||
this.opt.url +=
|
||
(/\?/.test(this.opt.url) ? '&' : '?') +
|
||
'_=' +
|
||
Math.random();
|
||
} else {
|
||
if (this.opt.formType === 'form-data') {
|
||
delete this.opt.headers['content-type'];
|
||
this.opt.param = this.formData();
|
||
} else if (this.opt.formType !== 'form') {
|
||
this.opt.param = JSON.stringify(this.opt.data);
|
||
}
|
||
}
|
||
|
||
//jsonp
|
||
if (this.opt.type === 'JSONP') {
|
||
this.transport = doc.createElement('script');
|
||
this.transport.onerror = this.transport.onload = function() {
|
||
_this.dispatch(_this.transport);
|
||
};
|
||
this.transport.src = this.opt.url;
|
||
doc.head.insertBefore(this.transport, doc.head.firstChild);
|
||
|
||
//6. 超时处理
|
||
if (this.opt.timeout && this.opt.timeout > 0) {
|
||
this.opt.timeoutID = setTimeout(function() {
|
||
_this.transport.onerror = _this.transport.onload = null;
|
||
_this.dispatch(null);
|
||
}, this.opt.timeout);
|
||
}
|
||
} else {
|
||
this.xhr.onreadystatechange = function(ev) {
|
||
if (_this.opt.timeout && _this.opt.timeout > 0) {
|
||
_this.opt['time' + this.readyState] = ev.timeStamp;
|
||
if (this.readyState === 4) {
|
||
_this.opt.isTimeout =
|
||
_this.opt.time4 - _this.opt.time1 >
|
||
_this.opt.timeout;
|
||
}
|
||
}
|
||
|
||
if (this.readyState !== 4) {
|
||
return;
|
||
}
|
||
|
||
_this.dispatch(_this.opt.isTimeout ? null : _this.xhr);
|
||
};
|
||
|
||
// 6. 初始化xhr提交
|
||
this.xhr.open(this.opt.type, this.opt.url, true);
|
||
|
||
// 7. 设置头信息
|
||
for (var i in this.opt.headers) {
|
||
if (this.opt.headers[i])
|
||
this.xhr.setRequestHeader(i, this.opt.headers[i]);
|
||
}
|
||
|
||
// 8. 发起网络请求
|
||
_this.xhr.send(_this.opt.param);
|
||
|
||
//超时处理
|
||
if (this.opt.timeout && this.opt.timeout > 0) {
|
||
this.xhr.timeout = this.opt.timeout;
|
||
}
|
||
}
|
||
return this.defer.promise;
|
||
};
|
||
|
||
// ---------------------- end ------------------------
|
||
|
||
if (!win.request) {
|
||
win.request = {
|
||
get: function(url) {
|
||
if (!url) throw new Error('argument url is required');
|
||
|
||
return new _request(url, 'GET');
|
||
},
|
||
post: function(url) {
|
||
if (!url) throw new Error('argument url is required');
|
||
|
||
return new _request(url, 'POST');
|
||
},
|
||
jsonp: function(url) {
|
||
if (!url) throw new Error('argument url is required');
|
||
|
||
return new _request(url, 'JSONP');
|
||
},
|
||
cache: {},
|
||
cid: 0,
|
||
version: '1.1.0-es5'
|
||
};
|
||
yua.ui.request = request.version;
|
||
}
|
||
|
||
return request;
|
||
});
|
JavaScript
95.2%
CSS
4.8%