/** * 新一代版本 * @author yutent * @date 2020/07/31 18:59:47 */ import { Format, toS } from './lib/format.js' const nativeFetch = window.fetch const NOBODY_METHODS = ['GET', 'HEAD'] const FORM_TYPES = { form: 'application/x-www-form-urlencoded; charset=UTF-8', json: 'application/json; charset=UTF-8', text: 'text/plain; charset=UTF-8' } const ERRORS = { 10001: 'Argument url is required', 10012: 'Parse error', 10100: 'Request canceled', 10104: 'Request pending...', 10200: 'Ok', 10204: 'No content', 10304: 'Not modified', 10500: 'Internal Server Error', 10504: 'Connected timeout' } class _Request { constructor(url = '', options = {}, owner) { if (!url) { throw new Error(ERRORS[10001]) } // url规范化 url = url.replace(/#.*$/, '') if (owner.BASE_URL) { if (!/^([a-z]+:|\/\/)/.test(url)) { url = owner.BASE_URL + url } } options.method = (options.method || 'get').toUpperCase() this._owner = owner this.options = { headers: { 'X-Requested-With': 'XMLHttpRequest', 'content-type': FORM_TYPES.form }, body: null, cache: 'default', signal: null, // 超时信号, 配置该项时, timeout不再生效 timeout: 30000 // 超时时间, 单位毫秒, 默认30秒 } if (!options.signal) { this.control = new AbortController() options.signal = this.control.signal } if (options.headers) { let headers = this.options.headers Object.assign(headers, options.headers) options.headers = headers } Object.assign(this.options, options, { url }) if (owner._inject_req) { owner._inject_req(this.options) } return this.__next__() } __next__() { var options = this.options var params = null var hasAttach = false // 是否有附件 var crossDomain = false // 是否跨域 var noBody = NOBODY_METHODS.includes(options.method) /* -------------------------- 1»» 请求的内容 --------------------- */ if (options.body) { var type = typeof options.body switch (type) { case 'number': case 'string': this.__type__('text') params = options.body break case 'object': // 解析表单DOM if (options.body.nodeName === 'FORM') { options.method = options.body.method.toUpperCase() || 'POST' params = Format.parseForm(options.body) hasAttach = params.constructor === FormData // 如果是一个 FormData对象,且为不允许携带body的方法,则直接改为POST } else if (options.body.constructor === FormData) { hasAttach = true if (noBody) { options.method = 'POST' } params = options.body } else { for (let k in options.body) { if (toS.call(options.body[k]) === '[object File]') { hasAttach = true break } } // 有附件,则改为FormData if (hasAttach) { if (noBody) { options.method = 'POST' } params = Format.mkFormData(options.body) } else { params = options.body } } } } if (hasAttach) { delete options.headers['content-type'] } /* -------------------------- 2»» 处理跨域 --------------------- */ try { let $a = document.createElement('a') $a.href = options.url crossDomain = location.protocol !== $a.protocol || location.host !== $a.host $a = null } catch (err) {} if (crossDomain && options.credentials === 'omit') { delete options.headers['X-Requested-With'] } /* ------------- 3»» 根据method类型, 处理表单数据 ---------------- */ // 拼接到url上 if (noBody) { params = Format.param(params) if (params) { options.url += (~options.url.indexOf('?') ? '&' : '?') + params } } else { if (!hasAttach) { if (~options.headers['content-type'].indexOf('json')) { params = JSON.stringify(params) } else { params = Format.param(params) } } } /* ----------------- 4»» 超时处理 -----------------------*/ if (options.timeout && options.timeout > 0) { this.timer = setTimeout(_ => { this.abort() }, options.timeout) delete options.timeout } /* ----------------- 5»» 构造请求 ------------------- */ var url = options.url delete options.url for (let k in options) { if (options[k] === null || options[k] === undefined || options[k] === '') { delete options[k] } } return nativeFetch(url, options) .then(r => { clearTimeout(this.timer) let isSucc = r.status >= 200 && r.status < 400 if (this._owner._inject_res) { r = this._owner._inject_res(r) } if (isSucc) { return r } else { return Promise.reject(r) } }) .catch(e => { clearTimeout(this.timer) return Promise.reject(e) }) } abort() { this.control.abort() } __type__(type) { this.options.headers['content-type'] = FORM_TYPES[type] } } function inject(target) { target.inject = { request(callback) { target._inject_req = callback }, response(callback) { target._inject_res = callback } } } const _fetch = function (url, options) { return new _Request(url, options, _fetch) } _fetch.create = function () { var another = function (url, options) { return new _Request(url, options, another) } inject(another) return another } inject(_fetch) export default _fetch