From ae80130dea4911a48d81c6413781b42db689136e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E5=A4=A9?= Date: Fri, 7 Aug 2020 19:39:28 +0800 Subject: [PATCH] 1.0.0 --- Readme.md | 65 ++++++- src/index.es7 | 258 ++++++++++++++++++++------ src/index222.es7 | 443 --------------------------------------------- src/lib/format.es7 | 113 +----------- src/next.es7 | 206 ++++++++++++++++++++- 5 files changed, 474 insertions(+), 611 deletions(-) delete mode 100644 src/index222.es7 diff --git a/Readme.md b/Readme.md index bb76e4d..56d6225 100644 --- a/Readme.md +++ b/Readme.md @@ -1,10 +1,69 @@ ## ajax的全新封装 -> 统一走fetch的风格。 +> 统一走fetch的风格。内置参数处理, 支持多实例。 ### 版本 -> 共有2个版本, 一个传统版本, 基于`XMLHttpRequest`; 另一个是新一代版本, 基于`window.fetch()`。2个版本功能基本一致, 使用上没有区别。 +> 共有2个版本, 一个传统版本, 基于`XMLHttpRequest`; 另一个是新一代版本, 基于`window.fetch()`。 `**注意:**` -由于`window.fetch()`只支持`http/https`协议, 所以在一些特殊的环境下(如electron等), 请使用传统版。 \ No newline at end of file +由于`window.fetch()`只支持`http/https`协议, 所以在一些特殊的环境下(如electron等), 请使用传统版。 + +### 2个版本的区别 + +1. 超时的返回值不一样。fetch版没有额外处理, 全由原生返回; 传统版为处理过, 统一返回`Response对象`。 + +2. 缓存参数不一致, 传统版只有传入`no-store`才不会缓存,其他任何值都会缓存, 缓存机制由headers及浏览器机制决定。 fetch版支持完整的参数, 详见原生fetch文档。 + +3. 验证机制,传参不一样。传统版credentials为布尔值; fetch版本则是支持omit, same-origin, include。 + + +### 示例 + +```js +import fetch from '//dist.bytedo.org/fetch/dist/index.js' // 传统版 +// import fetch from '//dist.bytedo.org/fetch/dist/next.js' // fetch版 + + +fetch('/get_list', {body: {page: 1}}) + .then(r => r.json()) + .then(list => { + console.log(list) + }) + + +// 创建一个新的fetch实例, 可传入新的基础域名, 和公共参数等 +var f1 = fetch.create('//192.168.1.101', {headers: {token: 123456}}) + +f1('/get_list', {body: {page: 1}}) + .then(r => r.json()) + .then(list => { + console.log(list) + }) + + +``` + + + +### APIs + +1. fetch(url[, options]) +> 发起一个网络请求, options的参数如下。 同时支持配置公共域名, 公共参数。 + + + method 默认GET, 可选GET/POST/PUT/DELETE... + + body 要发送的数据, 如果是GET方式, 会被自动拼接到url上 + + cache 是否缓存, + + credentials 是否校验 + + signal 网络控制信号, 可用于中断请求 + + timeout 超时时间, 默认30秒, 单位毫秒 + +```js +fetch.BASE_URL = '//192.168.1.100' +fetch.__INIT__ = {headers: {token: 123456}} + +``` + + +2. fetch.create([base_url][, options]) +> 创建一个新的fetch实例 \ No newline at end of file diff --git a/src/index.es7 b/src/index.es7 index 05c9213..9c5f3e6 100644 --- a/src/index.es7 +++ b/src/index.es7 @@ -4,9 +4,7 @@ * @date 2020/08/03 17:05:10 */ -import Format from './lib/format.js' - -const log = console.log +import { Format, toS } from './lib/format.js' const noop = function(e, res) { this.defer.resolve(res) @@ -30,24 +28,6 @@ const ERRORS = { 10504: 'Connected timeout' } -const CONVERT = { - text(val) { - return val - }, - xml(val, xml) { - return xml !== undefined ? xml : Format.parseXML(val) - }, - html(val) { - return Format.parseHTML(val) - }, - json(val) { - return JSON.parse(val) - }, - script(val) { - return Format.parseJS(val) - } -} - Promise.defer = function() { var _ = {} _.promise = new Promise(function(y, n) { @@ -57,10 +37,8 @@ Promise.defer = function() { return _ } -class _Instance {} - class _Request { - constructor(url = '', options = {}) { + constructor(url = '', options = {}, { BASE_URL, __INIT__ }) { if (!url) { throw new Error(ERRORS[10001]) } @@ -68,9 +46,9 @@ class _Request { // url规范化 url = url.replace(/#.*$/, '') - if (fetch.BASE_URL) { + if (BASE_URL) { if (!/^([a-z]+:|\/\/)/.test(url)) { - url = fetch.BASE_URL + url + url = BASE_URL + url } } @@ -86,18 +64,25 @@ class _Request { }, body: null, cache: 'default', - referrer: '', credentials: false, // 跨域选项,是否验证凭证 signal: null, // 超时信号, 配置该项时, timeout不再生效 timeout: 30000 // 超时时间, 单位毫秒, 默认30秒 } - // 取消网络请求 - // this.defer.promise.abort = () => { - // this.cancel = true - // this.xhr.abort() - // } - Object.assign(this.options, fetch.__INIT__, options, { url }) + if (!options.signal) { + var control = new AbortController() + options.signal = control.signal + } + this.defer.promise.abort = function() { + control.abort() + } + + if (__INIT__.headers && options.headers) { + Object.assign(__INIT__.headers, options.headers) + } + + Object.assign(this.options, __INIT__, options, { url }) + this.__next__() return this.defer.promise } @@ -107,15 +92,9 @@ class _Request { var params = null var hasAttach = false // 是否有附件 var crossDomain = false // 是否跨域 - var control = new AbortController() + var noBody = NOBODY_METHODS.includes(options.method) - /* ------------------------ 1»» 处理超时 ---------------------- */ - // 如果有传入signal, 则删除timeout配置 - if (options.signal) { - delete options.timeout - } else { - options.signal = control.signal - } + /* ------------------------ 1»» 处理signal ---------------------- */ options.signal.onabort = _ => { this.cancel = true this.xhr.abort() @@ -138,19 +117,25 @@ class _Request { params = Format.parseForm(options.body) hasAttach = params.constructor === FormData - if (hasAttach) { - delete options.headers['content-type'] - } - // 如果是一个 FormData对象 - // 则直接改为POST + // 如果是一个 FormData对象,且为不允许携带body的方法,则直接改为POST } else if (options.body.constructor === FormData) { hasAttach = true - options.method = 'POST' + if (noBody) { + options.method = 'POST' + } params = options.body - delete options.headers['content-type'] } 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 @@ -158,6 +143,9 @@ class _Request { } } } + if (hasAttach) { + delete options.headers['content-type'] + } /* -------------------------- 3»» 处理跨域 --------------------- */ try { @@ -176,21 +164,172 @@ class _Request { } } - /* ------------- 4»» 根据method类型, 处理g表单数据 ---------------- */ + /* ------------- 4»» 根据method类型, 处理表单数据 ---------------- */ + + // 拼接到url上 + if (noBody) { + params = Format.param(params) + if (params) { + options.url += (~options.url.indexOf('?') ? '&' : '?') + params + } + if (options.cache === 'no-store') { + options.url += + (~options.url.indexOf('?') ? '&' : '?') + '_t_=' + Date.now() + } + } else { + if (!hasAttach) { + if (~options.headers['content-type'].indexOf('json')) { + params = JSON.stringify(params) + } else { + params = Format.param(params) + } + } + } + + /* ----------------- 5»» 设置响应的数据类型 ---------------- */ + // 统一使用blob, 再转为其他的 + this.xhr.responseType = 'blob' + + /* ----------------- 6»» 构造请求 ------------------- */ + // 6.1 + this.xhr.onreadystatechange = ev => { + if (options.timeout > 0) { + options['time' + this.xhr.readyState] = ev.timeStamp + if (this.xhr.readyState === 4) { + options.isTimeout = options.time4 - options.time1 > options.timeout + } + } + + if (this.xhr.readyState !== 4) { + return + } + + this.__dispatch__(options.isTimeout) + } + + // 6.2»» 初始化xhr + this.xhr.open(options.method, options.url) + + // 6.3»» 设置头信息 + for (let k in options.headers) { + this.xhr.setRequestHeader(k, options.headers[k]) + } + + // 6.4»» 发起网络请求 + this.xhr.send(params) + + // 6.5»» 超时处理 + if (options.timeout && options.timeout > 0) { + this.xhr.timeout = options.timeout + } } __type__(type) { this.options.headers['content-type'] = FORM_TYPES[type] } + + __dispatch__(isTimeout) { + let result = { + status: 200, + statusText: 'ok', + body: '', + headers: Object.create(null) + } + + // 主动取消 + if (this.cancel) { + return this.__cancel__() + } + + // 超时 + if (isTimeout) { + return this.__timeout__() + } + + // 是否请求成功(resful规范) + let isSucc = this.xhr.status >= 200 && this.xhr.status < 400 + let headers = this.xhr.getAllResponseHeaders().split('\n') || [] + + //处理返回的 Header, 拿到content-type + for (let it of headers) { + it = it.trim() + if (it) { + it = it.split(':') + let k = it.shift().toLowerCase() + it = it.join(':').trim() + result.headers[k] = it + } + } + + if (isSucc) { + result.status = this.xhr.status + if (result.status === 204) { + result.statusText = ERRORS[10204] + } else if (result.status === 304) { + result.statusText = ERRORS[10304] + } + } else { + result.status = this.xhr.status || 500 + result.statusText = this.xhr.statusText || ERRORS[10500] + } + + result.body = this.xhr.response + + this.__success__(isSucc, result) + } + + __success__(isSucc, result) { + var response = new _Response( + result.status, + result.statusText, + result.body, + result.headers + ) + + if (isSucc) { + this.defer.resolve(response) + } else { + this.defer.reject(response) + } + delete this.xhr + delete this.options + delete this.defer + } + + __cancel__(result) { + var response = new _Response(0, ERRORS[10100], Object.create(null)) + + this.defer.reject(response) + + delete this.xhr + delete this.options + delete this.defer + } + + __timeout__(result) { + var response = new _Response(504, ERRORS[10504], Object.create(null)) + + this.defer.reject(response) + + delete this.xhr + delete this.options + delete this.defer + } } class _Response { - constructor(status = 200, data = null, headers = {}) { + constructor(status = 200, statusText = 'OK', data = null, headers = {}) { this.status = status - this.statusText = 'OK' - this.ok = true + this.statusText = statusText + this.ok = status >= 200 && status < 400 this.headers = headers - this.__R__ = data + + Object.defineProperty(this, '__R__', { + value: data, + writable: true, + enumerable: false, + configurable: true + }) } text() { @@ -212,8 +351,17 @@ class _Response { } } -function _fetch(url, param) { - return new _Request(url, param) +const _fetch = function(url, options) { + return new _Request(url, options, { + BASE_URL: _fetch.BASE_URL, + __INIT__: _fetch.__INIT__ || Object.create(null) + }) +} + +_fetch.create = function(BASE_URL, __INIT__ = Object.create(null)) { + return function(url, options) { + return new _Request(url, options, { BASE_URL, __INIT__ }) + } } export default _fetch diff --git a/src/index222.es7 b/src/index222.es7 deleted file mode 100644 index 2566081..0000000 --- a/src/index222.es7 +++ /dev/null @@ -1,443 +0,0 @@ -/** - * - * @authors yutent (yutent.io@gmail.com) - * @date 2018-03-25 23:59:13 - * @version $Id$ - */ - -import Format from './lib/format.js' - -// 本地协议/头 判断正则 -// const rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/ -const log = console.log - -const noop = function(e, res) { - this.defer.resolve(res) -} - -// let isLocal = false -// try { -// isLocal = rlocalProtocol.test(location.protocol) -// } catch (e) {} - -let originAnchor = document.createElement('a') -originAnchor.href = location.href - -const NOBODY_METHODS = ['GET', 'HEAD'] -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' -} - -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 convert = { - text(val) { - return val - }, - xml(val, xml) { - return xml !== undefined ? xml : Format.parseXML(val) - }, - html(val) { - return Format.parseHTML(val) - }, - json(val) { - return JSON.parse(val) - }, - script(val) { - return Format.parseJS(val) - } -} - -class _Request { - constructor(url = '', method = 'GET', param = {}) { - if (!url) { - throw new Error(ERRORS[10001]) - } - - // url规范化 - url = url.replace(/#.*$/, '') - - if (fetch.BASE_URL) { - if (!/^([a-z]+:|\/\/)/.test(url)) { - url = fetch.BASE_URL + url - } - } - - method = method.toUpperCase() - - this.xhr = new XMLHttpRequest() - this.defer = Promise.defer() - this.opt = { - url, - method, - headers: {}, - data: {}, - dataType: 'blob', - withCredentials: false // 跨域选项,是否验证凭证 - } - - // 取消网络请求 - this.defer.promise.abort = () => { - this.cancel = true - this.xhr.abort() - } - this.__next__(Object.assign({}, fetch.__INIT__, param)) - return this.defer.promise - } - - __next__(param) { - /* -------------------------------------------------------------- */ - /* ------------------------ 1»» 配置头信息 ---------------------- */ - /* -------------------------------------------------------------- */ - if (param.headers) { - Object.assign(this.opt.headers, param.headers) - } - - /* -------------------------------------------------------------- */ - /* --------- 2»» 设置表单类型, 其中 form-data不能手动设置 ---------- */ - /* -------------------------------------------------------------- */ - let hasAttach = false - if (param.formType) { - switch (param.formType) { - case 'form': - this.__set__('form') - break - case 'json': - this.__set__('json') - break - case 'form-data': - this.opt.method = 'POST' - hasAttach = true - break - default: - if (NOBODY_METHODS.includes(this.opt.method)) { - this.__set__('form') - } else { - this.__set__('text') - } - } - } else { - this.__set__('form') - } - - /* -------------------------------------------------------------- */ - /* ------------------- 3»» 设置缓存 ---------------------------- */ - /* -------------------------------------------------------------- */ - if (param.cache) { - if (NOBODY_METHODS.includes(this.opt.method)) { - this.opt.cache = true - } - } - - /* -------------------------------------------------------------- */ - /* ------------------- 4»» 设置超时时间(毫秒) --------------------- */ - /* -------------------------------------------------------------- */ - param.timeout = param.timeout >>> 0 - if (param.timeout > 0) { - this.opt.timeout = param.timeout - } - - /* -------------------------------------------------------------- */ - /* -------------------------- 5»» 请求的内容 --------------------- */ - /* -------------------------------------------------------------- */ - if (param.data) { - let type = typeof param.data - - switch (type) { - case 'number': - case 'string': - this.__set__('text') - this.opt.data = param.data - break - case 'object': - // 解析表单DOM - if (param.data.nodeName === 'FORM') { - this.opt.method = param.data.method.toUpperCase() || 'POST' - - this.opt.data = Format.parseForm(param.data) - hasAttach = this.opt.data.constructor === FormData - - if (hasAttach) { - delete this.opt.headers['content-type'] - } - // 如果是一个 FormData对象 - // 则直接改为POST - } else if (param.data.constructor === FormData) { - hasAttach = true - this.opt.method = 'POST' - delete this.opt.headers['content-type'] - this.opt.data = param.data - } else { - // 有附件,则改为FormData - if (hasAttach) { - this.opt.data = Format.mkFormData(param.data) - } else { - this.opt.data = param.data - } - } - } - } - - /* -------------------------------------------------------------- */ - /* -------------------------- 6»» 处理跨域 --------------------- */ - /* -------------------------------------------------------------- */ - if (param.withCredentials) { - this.opt.withCredentials = true - } - try { - let anchor = document.createElement('a') - anchor.href = this.opt.url - - this.opt.crossDomain = - originAnchor.protocol !== anchor.protocol || - originAnchor.host !== anchor.host - } catch (err) {} - - // 6.1»» 进一步处理跨域 - // 非跨域或跨域但支持Cors时自动加上一条header信息,用以标识这是ajax请求 - // 如果是跨域,开启Cors会需要服务端额外返回一些headers - - if (this.opt.crossDomain) { - if (this.opt.withCredentials) { - this.xhr.withCredentials = true - this.opt.headers['X-Requested-With'] = 'XMLHttpRequest' - } - } else { - this.opt.headers['X-Requested-With'] = 'XMLHttpRequest' - } - - /* -------------------------------------------------------------- */ - /* ------------- 7»» 根据method类型, 处理g表单数据 ---------------- */ - /* -------------------------------------------------------------- */ - // 是否允许发送body - let allowBody = !NOBODY_METHODS.includes(this.opt.method) - if (allowBody) { - if (!hasAttach) { - if (param.formType === 'json') { - this.opt.data = JSON.stringify(this.opt.data) - } else { - this.opt.data = Format.param(this.opt.data) - } - } - } else { - // 否则拼接到url上 - this.opt.data = Format.param(this.opt.data) - - if (this.opt.data) { - this.opt.url += (/\?/.test(this.opt.url) ? '&' : '?') + this.opt.data - } - - if (this.opt.cache === false) { - this.opt.url += - (/\?/.test(this.opt.url) ? '&' : '?') + '_=' + Math.random() - } - } - - /* -------------------------------------------------------------- */ - /* ------------- 8»» 设置响应的数据类型 ---------------- */ - /* -------------------------------------------------------------- */ - // arraybuffer | blob | document | json | text - if (param.dataType) { - this.opt.dataType = param.dataType.toLowerCase() - } - this.xhr.responseType = this.opt.dataType - - /* -------------------------------------------------------------- */ - /* ------------- 9»» 构造请求 ---------------- */ - /* -------------------------------------------------------------- */ - - // response ready - this.xhr.onreadystatechange = ev => { - if (this.opt.timeout > 0) { - this.opt['time' + this.xhr.readyState] = ev.timeStamp - if (this.xhr.readyState === 4) { - this.opt.isTimeout = - this.opt.time4 - this.opt.time1 > this.opt.timeout - } - } - - if (this.xhr.readyState !== 4) { - return - } - - this.__dispatch__(this.opt.isTimeout) - } - - // 9.1»» 初始化xhr - this.xhr.open(this.opt.method, this.opt.url, true) - - // 9.2»» 设置头信息 - for (let i in this.opt.headers) { - this.xhr.setRequestHeader(i, this.opt.headers[i]) - } - - // 9.3»» 发起网络请求 - this.xhr.send(this.opt.data) - - // 9.4»» 超时处理 - if (this.opt.timeout && this.opt.timeout > 0) { - this.xhr.timeout = this.opt.timeout - } - } - - __set__(type) { - this.opt.headers['content-type'] = FORM_TYPES[type] - } - - __dispatch__(isTimeout) { - let result = { - status: 200, - statusText: 'ok', - text: '', - body: '', - error: null - } - - // 主动取消 - if (this.cancel) { - return this.__cancel__(result) - } - - // 超时 - if (isTimeout) { - return this.__timeout__(result) - } - - // 是否请求成功(resful规范) - let isSucc = this.xhr.status >= 200 && this.xhr.status < 400 - - let headers = this.xhr.getAllResponseHeaders().split('\n') || [] - let contentType = '' - - //处理返回的 Header, 拿到content-type - for (let it of headers) { - it = it.trim() - if (it) { - it = it.split(':') - let tmp = it.shift().toLowerCase() - if (tmp === 'content-type') { - contentType = it - .join(':') - .trim() - .toLowerCase() - break - } - } - } - - if (isSucc) { - result.status = this.xhr.status - if (result.status === 204) { - result.statusText = ERRORS[10204] - } else if (result.status === 304) { - result.statusText = ERRORS[10304] - } - } else { - result.status = this.xhr.status || 500 - result.statusText = this.xhr.statusText || ERRORS[10500] - result.error = new Error(result.statusText) - } - // log(this.opt.dataType, this.xhr) - switch (this.opt.dataType) { - case 'arraybuffer': - case 'blob': - case 'document': - case 'json': - result.text = result.body = this.xhr.response - break - // text - default: - try { - //处理返回的数据 - let dataType = contentType.match(/json|xml|script|html/) - - dataType = (dataType && dataType[0].toLowerCase()) || 'text' - - result.text = this.xhr.response - result.body = convert[dataType](result.text, this.xhr.response) - } catch (err) { - result.error = err - result.statusText = ERRORS[10012] - } - break - } - this.__success__(isSucc, result) - } - - __success__(isSucc, result) { - if (isSucc) { - this.defer.resolve(result) - } else { - this.defer.reject(result) - } - delete this.xhr - delete this.opt - delete this.defer - } - - __cancel__(result) { - result.status = 0 - result.statusText = ERRORS[10100] - result.error = new Error(ERRORS[10100]) - - this.defer.reject(result) - - delete this.xhr - delete this.opt - delete this.defer - } - - __timeout__(result) { - result.status = 504 - result.statusText = ERRORS[10504] - result.error = new Error(ERRORS[10504]) - - this.defer.reject(result) - - delete this.xhr - delete this.opt - delete this.defer - } -} - -function _fetch(url, method = 'GET', param = {}) { - if (typeof method === 'object') { - param = method - method = 'GET' - } - return new _Request(url, method, param) -} - -_fetch.get = function(url, param = {}) { - return new _Request(url, 'GET', param) -} -_fetch.post = function(url, param = {}) { - return new _Request(url, 'POST', param) -} -_fetch.upload = function(url, param = {}) { - param.formType = 'form-data' - return this.post(url, param) -} -_fetch.download = function(url, param = {}) { - param.dataType = 'blob' - return this.get(url, param) -} - -_fetch.version = '2.0.0-normal' -_fetch.init = function(param = {}) { - this.__INIT__ = param -} - -export default _fetch diff --git a/src/lib/format.es7 b/src/lib/format.es7 index b29d7c2..c99cc48 100644 --- a/src/lib/format.es7 +++ b/src/lib/format.es7 @@ -5,46 +5,9 @@ * */ -const toS = Object.prototype.toString -const doc = window.document -const encode = encodeURIComponent -const decode = decodeURIComponent -const svgTags = - 'circle,defs,ellipse,image,line,path,polygon,polyline,rect,symbol,text,use' - -const TagHooks = function() { - this.option = doc.createElement('select') - this.thead = doc.createElement('table') - this.td = doc.createElement('tr') - this.area = doc.createElement('map') - this.tr = doc.createElement('tbody') - this.col = doc.createElement('colgroup') - this.legend = doc.createElement('fieldset') - this._default = doc.createElement('div') - this.g = doc.createElementNS('http://www.w3.org/2000/svg', 'svg') - - this.optgroup = this.option - this.tbody = this.tfoot = this.colgroup = this.caption = this.thead - this.th = this.td - - // 处理svg - svgTags.split(',').forEach(m => { - this[m] = this.g - }) -} - -const Helper = { - tagHooks: new TagHooks(), - rtagName: /<([\w:]+)/, - rxhtml: /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - scriptTypes: { - 'text/javascript': 1, - 'text/ecmascript': 1, - 'application/ecmascript': 1, - 'application/javascript': 1 - }, - rhtml: /<|&#?\w+;/ -} +export const toS = Object.prototype.toString +export const encode = encodeURIComponent +export const decode = decodeURIComponent /** * 表单序列化 @@ -72,75 +35,7 @@ function serialize(p, obj, query) { } } -export default { - parseJS(code) { - code = (code + '').trim() - if (code) { - if (code.indexOf('use strict') === 1) { - let script = doc.createElement('script') - script.text = code - doc.head.appendChild(script).parentNode.removeChild(script) - } else { - eval(code) - } - } - }, - parseXML(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(html) { - let fragment = doc.createDocumentFragment().cloneNode(false) - - if (typeof html !== 'string') { - return fragment - } - - if (!Helper.rhtml.test(html)) { - fragment.appendChild(document.createTextNode(html)) - return fragment - } - - html = html.replace(Helper.rxhtml, '<$1>').trim() - let tag = (Helper.rtagName.exec(html) || ['', ''])[1].toLowerCase() - let wrap = Helper.tagHooks[tag] || Helper.tagHooks._default - let firstChild = null - - //使用innerHTML生成的script节点不会触发请求与执行text属性 - wrap.innerHTML = html - let script = wrap.getElementsByTagName('script') - if (script.length) { - for (let i = 0, el; (el = script[i++]); ) { - if (Helper.scriptTypes[el.type]) { - let tmp = doc.createElement('script').cloneNode(false) - el.attributes.forEach(function(attr) { - tmp.setAttribute(attr.name, attr.value) - }) - tmp.text = el.text - el.parentNode.replaceChild(tmp, el) - } - } - } - - while ((firstChild = wrap.firstChild)) { - fragment.appendChild(firstChild) - } - - return fragment - }, +export const Format = { parseForm(form) { let data = {} let hasAttach = false diff --git a/src/next.es7 b/src/next.es7 index bf2fc29..a75ae86 100644 --- a/src/next.es7 +++ b/src/next.es7 @@ -4,4 +4,208 @@ * @date 2020/07/31 18:59:47 */ -import Format from './lib/format.es7' +import { Format, toS } from './lib/format.js' + +const noop = function(e, res) { + this.defer.resolve(res) +} + +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' +} + +class _Request { + constructor(url = '', options = {}, { BASE_URL, __INIT__ }) { + if (!url) { + throw new Error('Argument url is required') + } + + // url规范化 + url = url.replace(/#.*$/, '') + + if (BASE_URL) { + if (!/^([a-z]+:|\/\/)/.test(url)) { + url = BASE_URL + url + } + } + + options.method = (options.method || 'get').toUpperCase() + + 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 (__INIT__.headers && options.headers) { + Object.assign(__INIT__.headers, options.headers) + } + + Object.assign(this.options, __INIT__, options, { url }) + + 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 + } 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 + } + if (options.cache === 'no-store') { + options.url += + (~options.url.indexOf('?') ? '&' : '?') + '_t_=' + Date.now() + } + } 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 window + .fetch(url, options) + .then(r => { + clearTimeout(this.timer) + var isSucc = r.status >= 200 && r.status < 400 + 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] + } +} + +const _fetch = function(url, options) { + return new _Request(url, options, { + BASE_URL: _fetch.BASE_URL, + __INIT__: _fetch.__INIT__ || Object.create(null) + }) +} + +_fetch.create = function(BASE_URL, __INIT__ = Object.create(null)) { + return function(url, options) { + return new _Request(url, options, { BASE_URL, __INIT__ }) + } +} + +export default _fetch