/** * Request组件, modern版, 支持IE9+,chrome,FF * @authors yutent (yutent@doui.cc) * @date 2016-11-27 13:08:40 * */ 'use strict' import 'promise/index' import Format from './lib/format' 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() // --------------------------------------------------------- // -------------------- request 模块开始 -------------------- // --------------------------------------------------------- var requestConvert = { text: function(val) { return val }, xml: function(val, xml) { return xml !== undefined ? xml : Format.parseXML(val) }, html: function(val) { return Format.parseHTML(val) }, json: function(val) { return JSON.parse(val) }, script: function(val) { return Format.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 = Format.parseForm(this.opt.form) Format.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 = Format.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 = {} Format.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') { Format.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') Format.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 = Format.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') }, cache: {}, cid: 0, version: '1.1.0-light' } Anot.ui.request = request.version } export default request