master
宇天 2020-07-31 19:01:15 +08:00
parent 554fc20cfa
commit 5c203ad5f5
5 changed files with 709 additions and 0 deletions

10
Readme.md Normal file
View File

@ -0,0 +1,10 @@
## ajax的全新封装
> 统一走fetch的风格。
### 版本
> 共有2个版本, 一个传统版本, 基于`XMLHttpRequest`; 另一个是新一代版本, 基于`window.fetch()`。2个版本功能基本一致, 使用上没有区别。
`**注意:**`
由于`window.fetch()`只支持`http/https`协议, 所以在一些特殊的环境下(如electron等), 请使用传统版。

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "@bytedo/fetch",
"version": "1.0.0",
"description": "全新的ajax封装。分2个版本, 一个基于XMLHttpRequest, 一个基于window.fetch",
"main": "dist/index.js",
"directories": {
"lib": "dist/lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bytedo/fetch.git"
},
"keywords": ["fetch", "axois", "request", "ajax"],
"author": "yutent",
"license": "MIT",
"bugs": {
"url": "https://github.com/bytedo/fetch/issues"
},
"homepage": "https://github.com/bytedo/fetch#readme"
}

445
src/index.js Normal file
View File

@ -0,0 +1,445 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2018-03-25 23:59:13
* @version $Id$
*/
import Format from './lib/format'
// 本地协议/头 判断正则
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 (request.BASE_URL) {
if (!/^([a-z]+:|\/\/)/.test(url)) {
url = request.BASE_URL + url
}
}
method = method.toUpperCase()
this.xhr = new XMLHttpRequest()
this.defer = Promise.defer()
this.opt = {
url,
method,
headers: {},
data: {},
dataType: 'text',
withCredentials: false // 跨域选项,是否验证凭证
}
// 取消网络请求
this.defer.promise.abort = () => {
this.cancel = true
this.xhr.abort()
}
this.__next__(Object.assign({}, request.__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
}
}
if (!window.request) {
window.request = {
get(url, param = {}) {
return new _Request(url, 'GET', param)
},
post(url, param = {}) {
return new _Request(url, 'POST', param)
},
upload(url, param = {}) {
param.formType = 'form-data'
return this.post(url, param)
},
download(url, param = {}) {
param.dataType = 'blob'
return this.get(url, param)
},
open(url, method = 'GET', param = {}) {
if (typeof method === 'object') {
param = method
method = 'GET'
}
return new _Request(url, method, param)
},
version: '2.0.0-normal',
init(param = {}) {
this.__INIT__ = param
}
}
}
export default request

224
src/lib/format.js Normal file
View File

@ -0,0 +1,224 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2016-11-26 16:35:45
*
*/
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+;/
}
/**
* 表单序列化
*/
function serialize(p, obj, query) {
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, query)
} else {
query(k, it)
}
})
} else {
for (let i in obj) {
k = p ? `${p}[${i}]` : i
if (typeof obj[i] === 'object') {
serialize(k, obj[i], query)
} else {
query(k, obj[i])
}
}
}
}
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
},
parseForm(form) {
let data = {}
let hasAttach = false
for (let i = 0, field; (field = form.elements[i++]); ) {
switch (field.type) {
case 'select-one':
case 'select-multiple':
if (field.name.length && !field.disabled) {
for (let 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]
hasAttach = true
}
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
}
}
}
// 如果有附件, 改为FormData
if (hasAttach) {
return this.mkFormData(data)
} else {
return data
}
},
mkFormData(data) {
let form = new FormData()
for (let i in data) {
let el = data[i]
if (Array.isArray(el)) {
el.forEach(function(it) {
form.append(i + '[]', it)
})
} else {
form.append(i, data[i])
}
}
return form
},
param(obj) {
if (!obj || typeof obj === 'string' || typeof obj === 'number') {
return obj
}
let arr = []
let query = function(k, v) {
if (/native code/.test(v)) {
return
}
v = typeof v === 'function' ? v() : v
v = toS.call(v) === '[object File]' ? v : encode(v)
arr.push(encode(k) + '=' + v)
}
if (typeof obj === 'object') {
serialize('', obj, query)
}
return arr.join('&')
}
}

7
src/next.js Normal file
View File

@ -0,0 +1,7 @@
/**
* 新一代版本
* @author yutent<yutent.io@gmail.com>
* @date 2020/07/31 18:59:47
*/
import Format from './lib/format'