From 5c203ad5f54eb1be731bfa727b9530dbd08a57f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E5=A4=A9?= Date: Fri, 31 Jul 2020 19:01:15 +0800 Subject: [PATCH] update --- Readme.md | 10 ++ package.json | 23 +++ src/index.js | 445 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib/format.js | 224 +++++++++++++++++++++++ src/next.js | 7 + 5 files changed, 709 insertions(+) create mode 100644 Readme.md create mode 100644 package.json create mode 100644 src/index.js create mode 100644 src/lib/format.js create mode 100644 src/next.js diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..bb76e4d --- /dev/null +++ b/Readme.md @@ -0,0 +1,10 @@ +## ajax的全新封装 +> 统一走fetch的风格。 + + +### 版本 +> 共有2个版本, 一个传统版本, 基于`XMLHttpRequest`; 另一个是新一代版本, 基于`window.fetch()`。2个版本功能基本一致, 使用上没有区别。 + +`**注意:**` + +由于`window.fetch()`只支持`http/https`协议, 所以在一些特殊的环境下(如electron等), 请使用传统版。 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b31fa2 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..fbba382 --- /dev/null +++ b/src/index.js @@ -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 diff --git a/src/lib/format.js b/src/lib/format.js new file mode 100644 index 0000000..b29d7c2 --- /dev/null +++ b/src/lib/format.js @@ -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>').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('&') + } +} diff --git a/src/next.js b/src/next.js new file mode 100644 index 0000000..58e3bd2 --- /dev/null +++ b/src/next.js @@ -0,0 +1,7 @@ +/** + * 新一代版本 + * @author yutent + * @date 2020/07/31 18:59:47 + */ + +import Format from './lib/format'