master
宇天 2020-08-07 19:39:28 +08:00
parent 4b91e552fe
commit ae80130dea
5 changed files with 474 additions and 611 deletions

View File

@ -1,10 +1,69 @@
## ajax的全新封装
> 统一走fetch的风格。
> 统一走fetch的风格。内置参数处理, 支持多实例。
### 版本
> 共有2个版本, 一个传统版本, 基于`XMLHttpRequest`; 另一个是新一代版本, 基于`window.fetch()`。2个版本功能基本一致, 使用上没有区别。
> 共有2个版本, 一个传统版本, 基于`XMLHttpRequest`; 另一个是新一代版本, 基于`window.fetch()`。
`**注意:**`
由于`window.fetch()`只支持`http/https`协议, 所以在一些特殊的环境下(如electron等), 请使用传统版。
由于`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<Object>])
> 发起一个网络请求, options的参数如下。 同时支持配置公共域名, 公共参数。
+ method<String> 默认GET, 可选GET/POST/PUT/DELETE...
+ body<Any> 要发送的数据, 如果是GET方式, 会被自动拼接到url上
+ cache<String> 是否缓存,
+ credentials<String/Boolean> 是否校验
+ signal<Object> 网络控制信号, 可用于中断请求
+ timeout<Number> 超时时间, 默认30秒, 单位毫秒
```js
fetch.BASE_URL = '//192.168.1.100'
fetch.__INIT__ = {headers: {token: 123456}}
```
2. fetch.create([base_url][, options<Object>])
> 创建一个新的fetch实例

View File

@ -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

View File

@ -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

View File

@ -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></$2>').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

View File

@ -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