Fetch的使用及相容ie的處理
Fetch
作為一個與時俱進的前端,Fetch當然應該有所瞭解和涉獵。如果你沒有聽說過Fetch,那麼ajax應該不陌生吧。Fetch相當於是一個新版本的Ajax,雖然現在我們常常使用的仍是ajax,但是fetch已經在漸漸地撼動ajax的地位。在最近的專案中,為了更新一下技術棧,所以使用了fetch。所以寫一篇文章記錄一下fetch相關的內容。
先說一下fetch的優點吧,首先ajax最遭人詬病的就是回撥地獄了,也就是比如說如果你要傳送一個Ajax請求,但是請求的引數卻需要上一個ajax來返回。那麼這次的請求就需要放在上一次請求的回撥函式中,如果只有兩個請求還好,要是多個請求那麼程式碼不僅可讀性差,維護起來也十分的困難。在Es6中我們可以使用promise來解決回撥地獄的問題,實際上fetch的解決方式就是類似於使用promise的ajax,它的使用方式也類似於promise,使用起來程式碼的可讀性可維護性都變得更好了。
如果不瞭解promise的童鞋可以去看es6的文件,或者看相關的教程,這裡為了直奔主題就不講解promise了
先看一下MDN的官方文件
這是fetch的基本用法,第一個引數是url也就是你請求的地址,第二個引數接受一個配置物件,該物件的具體屬性以及可以設定的值已經在上圖展示。
具體引數配置如下:
一般來說使用fetch會用url加上配置物件的方式來發送請求,不過你也可以使用request建構函式例項化一個request物件作為引數傳入
在這裡主要講一下使用配置物件的方式來使用fetch
首先要進行請求,我們需要一個後臺介面,由於現如今開發模式基本上都是前後分離,所以我們的fetch請求不可避免的要涉及到跨域問題。
在下面的例子中,我使用的是nodejs,和express搭建的後臺。
由於我使用的是專案的後臺只是在app.js中加了一個測試介面所以就不貼出完整的app.js的程式碼了
我使用的後臺介面程式碼如下
app.all('/Api', function(req, res, next) {// 列印前端的資訊 console.log(req.body); console.log(req.cookies); console.log(req.get('Token')); res.header("Access-Control-Allow-Origin", "http://localhost:63342"); //設定請求的來源的域 res.header("Access-Control-Allow-Headers", "Token,x-token,Content-Type"); // 設定允許的自定義頭部 res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"); // 設定允許的請求的型別 res.header("Access-Control-Allow-Credentials",true); // 設定是否允許跨域請求攜帶cookie等瀏覽器資訊 res.header("X-Powered-By",'lhy'); res.header("Content-Type", "application/json;charset=utf-8"); // 設定返回的資料型別,這裡設定為返回的json型別的資料 res.send({meta:{token:"123",code:1}}); // 傳送響應資訊 });
前端使用fetch程式碼如下
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>Fetch</title> <meta name="Description" content=""/> <meta name="Author" content="lhy"/> </head> <body> <p></p> <script> let options = { method:"post", body:JSON.stringify({name:"lhy",content:"hello"}), // 這裡傳入的資料型別必須和下面content-type的型別一致 cache:'reload', // 表示請求時忽略http快取,但是請求完成後會重新整理http快取 credentials:'include', // 表示請求攜帶cookie等資訊 headers:{ 'Token':"lhytest", // 用於設定請求頭 'content-type': 'application/json' // 設定傳送的資料型別 } }; fetch('http://localhost/Api',options).then(function (response) { return response.json() }).then(function (data) { console.log(data); document.getElementsByTagName('p')[0].innerText = data.meta.token; }).catch(function (error) { console.log(error); }) </script> </body> </html>
PS:剛才在後臺設定的允許跨域的源我們可以在瀏覽器除錯視窗看到,而且如果你的html是本地環境開啟Origin的值會為null,我這裡是使用的webstrom開啟的
現在我們來看看結果
可以看到我們已經成功地拿到了後臺地資料,我們再去看看後臺是否也能拿到我傳遞的引數,以及cookie的資訊
PS:你設定的自定義頭部在瀏覽器除錯視窗無法看到,因為瀏覽器顯示的頭只顯示它預設規定的請求頭資訊,如果你希望在瀏覽器視窗看到就需要將它暴露出去,這裡不要再細說
Fetch的相容
在上面我們可以看到,fetch還是十分方便強大的,所有的新的這些好用的技術往往都有一個限制那就是相容問題
我們先看一下原生的fetch的相容性如何
這麼好用的東西,ie竟然完全不支援(垃圾ie毀我青春!!)
沒辦法,這可不是低版本ie不相容,而是ie完全不相容,雖然現在ie的市場份額在逐年下降,但是其使用者群體還是十分龐大的,而不巧的是這次的專案要求相容到ie8
這不是為難我胖虎嗎?我又不想捨棄好用的fetch,沒辦法那就自己封裝一個ie版本的fetch吧。
封裝一個ie版本的fetch,首先我們要了解這個fetch到底包含了些什麼,作為一個精緻的前端,我可不想直接呼叫fetch時檢測一下window下有沒有這個函式,沒有就用ajax的粗陋的方式。
所以就有了這篇文章的後半部分。
我們先來看看MDN的fetch的使用模組下有些什麼東西
所以我們要重新封裝一下 Body,Headers,Request,Response
本人很菜,下面的封裝方式全是我自己的看法,很有可能並不是fetch的內部實現方式,特此宣告。
參考文章:https://segmentfault.com/a/1190000006220369
主要思路:
檢測瀏覽器版本,是ie10,11使用XMLHttpRequest進行請求
ie8,9 使用XDomainRequest
ie8以下使用ActiveXObject進行請求
(function webpackUniversalModuleDefinition(root, factory) { if (typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if (typeof define === 'function' && define.amd) define([], factory); else if (typeof exports === 'object') exports["fetch"] = factory(); else root["fetch"] = factory(); })(this, function () { return /******/ (function (modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if (installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/([ /* 0 */ /***/ function (module, exports, __webpack_require__) { var Request = __webpack_require__(1) var Response = __webpack_require__(5) var Headers = __webpack_require__(2) var Transport = __webpack_require__(6) if (![].forEach) { Array.prototype.forEach = function (fn, scope) { 'use strict' var i, len for (i = 0, len = this.length; i < len; ++i) { if (i in this) { fn.call(scope, this[i], i, this) } } } } // 用於讀取響應頭資訊 if (!'lhy'.trim) { var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g String.prototype.trim = function () { return this.replace(rtrim, '') } } function headers(xhr) { var head = new Headers() if (xhr.getAllResponseHeaders) { var headerStr = xhr.getAllResponseHeaders() || '' if (/\S/.test(headerStr)) { //http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method var headerPairs = headerStr.split('\u000d\u000a'); for (var i = 0; i < headerPairs.length; i++) { var headerPair = headerPairs[i]; // 讀取header的資訊 var index = headerPair.indexOf('\u003a\u0020') if (index > 0) { var key = headerPair.substring(0, index).trim() var value = headerPair.substring(index + 2).trim() head.append(key, value) } } } } return head } function fetch(input, init) { return new Promise(function (resolve, reject) { var request if (!init && (init instanceof Request)) { request = input } else { request = new Request(input, init) } var msie = 11 // 用於判斷是否為ie if (window.VBArray) { // 返回瀏覽器渲染文件模式 msie = document.documentMode || (window.XMLHttpRequest ? 7 : 6) } if (msie > 7) { var xhr = new Transport(request) function responseURL() { if ('responseURL' in xhr) { return xhr.responseURL } return } xhr.on('load', function (event) { var options = { status: event.status || 200, statusText: event.statusText || '', headers: headers(event), url: responseURL() } var body = 'response' in event ? event.response : event.responseText resolve(new Response(body, options)) }) xhr.on('error', function () { reject(new TypeError('Network request failed')) }) xhr.on('timeout', function () { reject(new TypeError('Network request timeout')) }) xhr.open(request.method, request.url, true) request.headers.forEach(function (value, name) { xhr.setRequestHeader(name, value) }) xhr.send(typeof request._body === 'undefined' ? null : request._body) } else { var xhr = new ActiveXObject('Microsoft.XMLHTTP') xhr.onreadystatechange = function () { if (xhr.readyState === 4) { var options = { status: xhr.status || 200, statusText: xhr.statusText || '', headers: headers(xhr), url: responseURL() } var body = 'response' in xhr ? xhr.response : xhr.responseText resolve(new Response(body, options)) } } xhr.open(request.method, request.url, true) xhr.send(typeof request._body === 'undefined' ? null : request._body) } }) } function notFunc(a) { return !/\scode\]\s+\}$/.test(a) } if (notFunc(window.fetch)) { window.fetch = fetch } if (typeof avalon === 'function') { avalon.fetch = fetch } module.exports = fetch /***/ }, /* 1 */ /***/ function (module, exports, __webpack_require__) { var Headers = __webpack_require__(2) var Body = __webpack_require__(4) // 自定義Request函式 function Request(input, options) { options = options || {} var body = options.body // 用於判斷函式接受的引數是否為自定義的Request物件 即判斷input是否由Request建立 if (input instanceof Request) { // 判斷body是否已被使用 if (input.bodyUsed) { throw new TypeError('Already read') } this.url = input.url this.credentials = input.credentials if (!options.headers) { var h = this.headers = new Headers(input.headers) if (!h.map['x-requested-with']) { h.set('X-Requested-With', 'XMLHttpRequest') } } this.method = input.method this.mode = input.mode if (!body) { body = input._body input.bodyUsed = true } } else { // 如果input不是由Request建立的自定義Request物件 則input為url引數 this.url = input } // 優先判斷option中是否設定了相關選項,再判斷credentials自定義request物件的相關屬性,如果都沒有預設為‘omit’ this.credentials = options.credentials || this.credentials || 'omit' // 判斷引數是否設定了header的相關選項 if (options.headers || !this.headers) { this.headers = new Headers(options.headers) } this.method = (options.method || this.method || 'GET').toUpperCase() this.mode = options.mode || this.mode || null this.referrer = null // 如果是head請求卻攜帶了請求體,丟擲錯誤 if ( this.method === 'HEAD' && body) { throw new TypeError('Body not allowed for HEAD requests') }else if(this.method === 'GET' && body){ var Obody = JSON.parse(body) var str = '' for (var name in Obody) { if(Obody.hasOwnProperty(name)){ str = str? str + '&' + name + '=' + Obody[name] : str + name + '=' + Obody[name] } } this.url += '?' + str body = null } this._initBody(body) } Request.prototype.clone = function () { return new Request(this) } var F = function () { } F.prototype = Body.prototype Request.prototype = new F() module.exports = Request /***/ }, /* 2 */ /***/ function (module, exports, __webpack_require__) { var support = __webpack_require__(3) // 自定義Header function Headers(headers) { this.map = {} if (headers instanceof Headers) { headers.forEach(function (value, name) { this.append(name, value) }, this) } else if (headers) { for (var name in headers) { if (headers.hasOwnProperty(name)) { this.append(name, headers[name]) } } } } // 向header物件中的map 新增鍵值對 Headers.prototype.append = function (name, value) { name = normalizeName(name) value = normalizeValue(value) var list = this.map[name] if (!list) { list = [] this.map[name] = list } list.push(value) } // 定義header上的delet方法用於刪除鍵值對 Headers.prototype['delete'] = function (name) { delete this.map[normalizeName(name)] } // 用於獲取header物件上的某個鍵的第一個值 Headers.prototype.get = function (name) { var values = this.map[normalizeName(name)] return values ? values[0] : null } // 用於獲取header物件上某個鍵的所有值 Headers.prototype.getAll = function (name) { return this.map[normalizeName(name)] || [] } // 判斷該header物件是否擁有某個屬性 Headers.prototype.has = function (name) { return this.map.hasOwnProperty(normalizeName(name)) } // 用於設定該header物件上的值 Headers.prototype.set = function (name, value) { this.map[normalizeName(name)] = [normalizeValue(value)] } // 為了在低版本瀏覽器使用,定義forEach以遍歷 Headers.prototype.forEach = function (callback, thisArg) { for (var name in this.map) { if (this.map.hasOwnProperty(name)) { this.map[name].forEach(function (value) { callback.call(thisArg, value, name, this) }, this) } } } // 返回header物件的可列舉屬性及函式名 Headers.prototype.keys = function () { var items = [] this.forEach(function (value, name) { items.push(name) }) return items } // 返回header物件的所有可列舉的值 Headers.prototype.values = function () { var items = [] this.forEach(function (value) { items.push(value) }) return items } // 修改迭代器的方法 Headers.prototype.entries = function () { var items = [] this.forEach(function (value, name) { items.push([name, value]) }) return items } // 判斷是否支援迭代器 if (support.iterable) { // 如果支援 則讓header的iterable為上方的entries函式 Headers.prototype[Symbol.iterator] = Headers.prototype.entries } // 判斷頭名是否合法,只要不包含特殊字元就返回 頭名的字串 function normalizeName(name) { if (typeof name !== 'string') { name = String(name) } if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { throw new TypeError('Invalid character in header field name') } return name.toLowerCase() } // 將值轉為字串 function normalizeValue(value) { if (typeof value !== 'string') { value = String(value) } return value } module.exports = Headers /***/ }, /* 3 */ /** * 該函式用於判斷瀏覽器是否支援 * */ function (module, exports) { module.exports = { searchParams: 'URLSearchParams' in window, iterable: 'Symbol' in window && 'iterator' in window, blob: 'FileReader' in window && 'Blob' in window && (function () { try { new Blob() return true } catch (e) { return false } })(), formData: 'FormData' in window, arrayBuffer: 'ArrayBuffer' in window } /***/ }, /* 4 */ /***/ function (module, exports, __webpack_require__) { var support = __webpack_require__(3) // 用於建立body物件 function Body() { this.bodyUsed = false } var p = Body.prototype 'text,blob,formData,json,arrayBuffer'.replace(/\w+/g, function (method) { p[method] = function () { return consumeBody(this).then(function (body) { return convertBody(body, method) }) } }) // 初始化請求的頭部 p._initBody = function (body) { this._body = body if (!this.headers.get('content-type')) { var a = bodyType(body) switch (a) { case 'text': this.headers.set('content-type', 'text/plain;charset=UTF-8') break case 'blob': if (body && body.type) { this.headers.set('content-type', body.type) } break case 'searchParams': this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') break } } } // 判斷Body是否已被使用 function consumeBody(body) { if (body.bodyUsed) { return Promise.reject(new TypeError('Already read')) } else { body.bodyUsed = true return Promise.resolve(body._body) } } // 用於處理返回的response物件body的資料 function convertBody(body, to) { var from = bodyType(body) if (body === null || body === void 0 || !from || from === to) { return Promise.resolve(body) } else if (map[to] && map[to][from]) { return map[to][from](body) } else { return Promise.reject(new Error('Convertion from ' + from + ' to ' + to + ' not supported')) } } // 定義對各種型別資料的處理方法 var map = { text: { json: function (body) {//json --> text return Promise.resolve(JSON.stringify(body)) }, blob: function (body) {//blob --> text return blob2text(body) }, searchParams: function (body) {//searchParams --> text return Promise.resolve(body.toString()) } }, json: { text: function (body) {//text --> json return Promise.resolve(parseJSON(body)) }, blob: function (body) {//blob --> json return blob2text(body).then(parseJSON) } }, formData: { text: function (body) {//text --> formData return text2formData(body) } }, blob: { text: function (body) {//json --> blob return Promise.resolve(new Blob([body])) }, json: function (body) {//json --> blob return Promise.resolve(new Blob([JSON.stringify(body)])) } }, arrayBuffer: { blob: function (body) { return blob2ArrayBuffer(body) } } } // 用於返回body攜帶的資料型別 function bodyType(body) { if (typeof body === 'string') { return 'text' } else if (support.blob && (body instanceof Blob)) { return 'blob' } else if (support.formData && (body instanceof FormData)) { return 'formData' } else if (support.searchParams && (body instanceof URLSearchParams)) { return 'searchParams' } else if (body && typeof body === 'object') { return 'json' } else { return null } } // 用於低版本瀏覽器的reader function reader2Promise(reader) { return new Promise(function (resolve, reject) { reader.onload = function () { resolve(reader.result) } reader.onerror = function () { reject(reader.error) } }) } /* 模擬下列函式 用於處理各種型別的返回值資料 readAsBinaryString(File|Blob) readAsText(File|Blob [, encoding]) readAsDataURL(File|Blob) readAsArrayBuffer(File|Blob) */ function text2formData(body) { var form = new FormData() body.trim().split('&').forEach(function (bytes) { if (bytes) { var split = bytes.split('=') var name = split.shift().replace(/\+/g, ' ') var value = split.join('=').replace(/\+/g, ' ') form.append(decodeURIComponent(name), decodeURIComponent(value)) } }) return Promise.resolve(form) } function blob2ArrayBuffer(blob) { var reader = new FileReader() reader.readAsArrayBuffer(blob) return reader2Promise(reader) } function blob2text(blob) { var reader = new FileReader() reader.readAsText(blob) return reader2Promise(reader) } function parseJSON(body) { try