whatwg-fetch源码分析
fetch 是什么
XMLHttpRequest
的最新替代技术
fetch优点
- 接口更简单、简洁,更加语义化
- 基于promise,更加好的流程化控制,可以不断then把参数传递,外加 async/await,异步变同步的代码书写风格
- 利于同构,isomorphic-fetch 是对 whatwg-fetch和node-fetch的一种封装,你一份代码就可以在两种环境下跑起来了
- 新的web api很多内置支持fetch,比如 service worker
fetch 缺点
- 兼容性
- 不支持progress事件(可以借助
response.body.
getRender
方法来实现) - 默认不带cookie
- 某些错误的http状态下如400、500等不会reject,相反它会被resolve
- 不支持timeout处理
- 不支持jsonp,当然可以引入 fetch-jsonp 来支持
这些缺点,后面的参考里面有各种解决方案
fetch兼容性(2017-08-08):
fetch是基于promise设计的,
- 如果浏览器不支持promise,可以引入 es6-promise 或者 promise。
- 如果希望IE8也支持,可以参考 使用fetch遇到过的坑 或者使用 fetch-ie8
fetch参数
参考 Fetch Standard 或者 Using Fetch
上面你对fetch有基本的了解了,而且提供了不少的链接解惑,那么我们进入正题,whatwg-fetch源码分析
依旧是先删除无用的代码,
(function (self) { 'use strict'; if (self.fetch) { return } // 封装的 Headers,支持的方法参考https://developer.mozilla.org/en-US/docs/Web/API/Headers function Headers(headers) { ...... } //方法参考:https://developer.mozilla.org/en-US/docs/Web/API/Body function Body() { ...... } // 请求的Request对象 ,https://developer.mozilla.org/en-US/docs/Web/API/Request // cache,context,integrity,redirect,referrerPolicy 在MDN定义中是存在的 function Request(input, options) { ...... } Body.call(Request.prototype) //把Body方法属性绑到 Reques.prototype function Response(bodyInit, options) { } Body.call(Response.prototype) //把Body方法属性绑到 Reques.prototype self.Headers = Headers //暴露Headers self.Request = Request //暴露Request self.Response = Response //暴露Response self.fetch = function (input, init) { return new Promise(function (resolve, reject) { var request = new Request(input, init) //初始化request对象 var xhr = new XMLHttpRequest() // 初始化 xhr xhr.onload = function () { //请求成功,构建Response,并resolve进入下一阶段 var options = { status: xhr.status, statusText: xhr.statusText, headers: parseHeaders(xhr.getAllResponseHeaders() || '') } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') var body = 'response' in xhr ? xhr.response : xhr.responseText resolve(new Response(body, options)) } //请求失败,构建Error,并reject进入下一阶段 xhr.onerror = function () { reject(new TypeError('Network request failed')) } //请求超时,构建Error,并reject进入下一阶段 xhr.ontimeout = function () { reject(new TypeError('Network request failed')) } // 设置xhr参数 xhr.open(request.method, request.url, true) // 设置 credentials if (request.credentials === 'include') { xhr.withCredentials = true } else if (request.credentials === 'omit') { xhr.withCredentials = false } // 设置 responseType if ('responseType' in xhr && support.blob) { xhr.responseType = 'blob' } // 设置Header request.headers.forEach(function (value, name) { xhr.setRequestHeader(name, value) }) // 发送请求 xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } //标记是fetch是polyfill的,而不是原生的 self.fetch.polyfill = true })(typeof self !== 'undefined' ? self : this); // IIFE函数的参数,不用window,web worker, service worker里面也可以使用
简单分析一下
- 如果自身支持fetch,直接返回,用自身的
- 内部核心 Headers, Body, Request, Response,
- Request和Resonse原型上有Body的方法属性,或者说,继承了
- Headers,Request ,Reponse暴露到全局
- fetch本质就是对XMLHttpRequest 请求的封装
这么一看其实到没什么了,不过完整代码里面有一些东西还是提一下(后面的参考都有链接)
- Symbol, Iterator : ES6里面很多集合是自带默认Iterator的,作用就是在 let...of,数组解构,新Set,Map初始化等情况会被调用。
- DataView , TypedArray:都是对TypeArray读写的API
- Blob,FileReader :File API,这个也没啥多说的
-
URLSearchParams: 这个支持度还不高,用来解析和构建 URL Search 参数的,例如 new URLSearchParams(window.location.search).get('a')
对外暴露的对象或者方法有
- fetch
封装过后的fetch,关于参数和使用
- Headers
http请求头,属性方法和使用
- Request
请求对象 ,属性方法和使用
- Response
请求的响应对象,属性方法和使用
这面重点解析几个重点函数和方法,其他的相对容易
iteratorFor
在定义中,Headers实例,headers.keys(), headers.values(), headers.entries()返回的都是Iterator, 下面代码读起来可能有点绕,
你这样理解,定义iterator 是保证能使用next方法来遍历
定义iterator[Symbol.iterator] 是设置默认 Iterator,能使用 let...of,Array.from,数组解构等相对高级一些方法访问到
// 枚举器, http://es6.ruanyifeng.com/#docs/iterator // 觉得可以如下 ,同样支持 next() 和 for ...of 等形式访问 ,之后才是不支持iterable的情况,添加next方法来访问 // if ((support.iterable && items[Symbol.iterator]) { // return items[Symbol.iterator]() // } function iteratorFor(items) { // 这里你就可以 res.headers.keys().next().value这样调用 var iterator = { next: function () { var value = items.shift() return { done: value === undefined, value: value } } } if (support.iterable) { // 添加默认Iterator // for...of,解构赋值,扩展运算符,yield*,Map(), Set(), WeakMap(), WeakSet(),Promise.all(),Promise.race()都会调用默认Iterator iterator[Symbol.iterator] = function () { return iterator } } // 到这里就支持了两种访问形式了 // res.headers.keys().next().value // for(let key in headers.keys()) return iterator }
Body.call
实现继承,把body的方法属性绑定指定对象原型
Body.call(Request.prototype)
Body.call(Response.prototype)
这两个理解上,就基本可以无大碍了,那我贴出完整带注释的代码
(function (self) { 'use strict'; //如果自身支持fetch,直接返回原生的fetch if (self.fetch) { // return } // 一些功能检测 var support = { searchParams: 'URLSearchParams' in self, // queryString 处理函数,https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams,http://caniuse.com/#search=URLSearchParams iterable: 'Symbol' in self && 'iterator' in Symbol, // Symbol(http://es6.ruanyifeng.com/#docs/symbol)E6新数据类型,表示独一无二的值 和 iterator枚举 blob: 'FileReader' in self && 'Blob' in self && (function () { try { new Blob() return true } catch (e) { return false } })(), // Blob 和 FileReader formData: 'FormData' in self, // FormData arrayBuffer: 'ArrayBuffer' in self // ArrayBuffer 二进制数据存储 } // 支持的 ArrayBuffer类型 if (support.arrayBuffer) { var viewClasses = [ '[object Int8Array]', '[object Uint8Array]', '[object Uint8ClampedArray]', '[object Int16Array]', '[object Uint16Array]', '[object Int32Array]', '[object Uint32Array]', '[object Float32Array]', '[object Float64Array]' ] // 检查是不是DataView,DataView是来读写ArrayBuffer的 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView var isDataView = function (obj) { return obj && DataView.prototype.isPrototypeOf(obj) } // 检查是不是有效的ArrayBuffer view,TypedArray均返回true ArrayBuffer.isView(new ArrayBuffer(10)) 为false, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/isView var isArrayBufferView = ArrayBuffer.isView || function (obj) { return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 } } // 检查header name,并转为小写 function normalizeName(name) { // 不是字符串,转为字符串 if (typeof name !== 'string') { name = String(name) } // 不以 a-z 0-9 -#$%*+.^_`|~ 开头,抛出错误 if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { throw new TypeError('Invalid character in header field name') } //转为小写 return name.toLowerCase() } // 转换header的值 function normalizeValue(value) { if (typeof value !== 'string') { value = String(value) } return value } // 枚举器, http://es6.ruanyifeng.com/#docs/iterator // 觉得可以如下 ,同样支持 next() 和 for ...of 等形式访问 ,之后才是不支持iterable的情况,添加next方法来访问 // if ((support.iterable && items[Symbol.iterator]) { // return items[Symbol.iterator]() // } function iteratorFor(items) { // 这里你就可以 res.headers.keys().next().value这样调用 var iterator = { next: function () { var value = items.shift() return { done: value === undefined, value: value } } } if (support.iterable) { // 添加默认Iterator // for...of,解构赋值,扩展运算符,yield*,Map(), Set(), WeakMap(), WeakSet(),Promise.all(),Promise.race()都会调用默认Iterator iterator[Symbol.iterator] = function () { return iterator } } // 到这里就支持了两种访问形式了 // res.headers.keys().next().value // for(let key in headers.keys()) return iterator } // 封装的 Headers,支持的方法参考https://developer.mozilla.org/en-US/docs/Web/API/Headers function Headers(headers) { this.map = {} // headers 最终存储的地方 if (headers instanceof Headers) { // 如果已经是 Headers的实例,复制键值 headers.forEach(function (value, name) { this.append(name, value) }, this) // this修改forEach执行函数上下文为当前上下文,就可以直接调用append方法了 } else if (Array.isArray(headers)) { // 如果是数组,[['Content-Type':''],['Referer','']] headers.forEach(function (header) { this.append(header[0], header[1]) }, this) } else if (headers) { // 对象 {'Content-Type':'',Referer:''} Object.getOwnPropertyNames(headers).forEach(function (name) { this.append(name, headers[name]) }, this) } } // 添加或者追加Header Headers.prototype.append = function (name, value) { name = normalizeName(name) value = normalizeValue(value) var oldValue = this.map[name] // 支持 append, 比如 Accept:text/html ,后来 append('Accept','application/xhtml+xml') 那么最终 Accept:'text/html,application/xhtml+xml' this.map[name] = oldValue ? oldValue + ',' + value : value } //删除名为name的Header Headers.prototype['delete'] = function (name) { delete this.map[normalizeName(name)] } //获得名为Name的Header Headers.prototype.get = function (name) { name = normalizeName(name) return this.has(name) ? this.map[name] : null } //查询时候有名为name的Header Headers.prototype.has = function (name) { return this.map.hasOwnProperty(normalizeName(name)) } //设置或者覆盖名为name,值为vaue的Header Headers.prototype.set = function (name, value) { this.map[normalizeName(name)] = normalizeValue(value) } //遍历Headers Headers.prototype.forEach = function (callback, thisArg) { //遍历属性 //我觉得也挺不错 Object.getOwnPropertyNames(this.map).forEach(function(name){ callback.call(thisArg, this.map[name], name, this) },this) for (var name in this.map) { //检查是不是自己的属性 if (this.map.hasOwnProperty(name)) { //调用 callback.call(thisArg, this.map[name], name, this) } } } // 所有的键,keys, values, entries, res.headers返回的均是 iterator Headers.prototype.keys = function () { var items = [] this.forEach(function (value, name) { items.push(name) }) return iteratorFor(items) } // 所有的值,keys, values, entries, res.headers返回的均是 iterator Headers.prototype.values = function () { var items = [] this.forEach(function (value) { items.push(value) }) return iteratorFor(items) } // 所有的entries,格式是这样 [[name1,value1],[name2,value2]],keys, values, entries, res.headers返回的均是 iterator Headers.prototype.entries = function () { var items = [] this.forEach(function (value, name) { items.push([name, value]) }) return iteratorFor(items) } //设置Headers原型默认的Iterator,keys, values, entries, res.headers返回的均是 iterator if (support.iterable) { Headers.prototype[Symbol.iterator] = Headers.prototype.entries } //是否已经消费/读取过,如果读取过,会直接到catch或者error处理函数 function consumed(body) { if (body.bodyUsed) { return Promise.reject(new TypeError('Already read')) } body.bodyUsed = true } // FileReader读取完毕 function fileReaderReady(reader) { return new Promise(function (resolve, reject) { reader.onload = function () { resolve(reader.result) } reader.onerror = function () { reject(reader.error) } }) } // 读取blob为ArrayBuffer对象,https://www.w3.org/TR/FileAPI/#dfn-filereader function readBlobAsArrayBuffer(blob) { var reader = new FileReader() var promise = fileReaderReady(reader) reader.readAsArrayBuffer(blob) return promise } // 读取blob为文本,https://www.w3.org/TR/FileAPI/#dfn-filereader function readBlobAsText(blob) { var reader = new FileReader() var promise = fileReaderReady(reader) reader.readAsText(blob) return promise } // ArrayBuffer读为文本 function readArrayBufferAsText(buf) { var view = new Uint8Array(buf) var chars = new Array(view.length) for (var i = 0; i < view.length; i++) { chars[i] = String.fromCharCode(view[i]) } return chars.join('') } //克隆ArrayBuffer function bufferClone(buf) { if (buf.slice) { //支持 slice,直接slice(0)复制,数据基本都是这样复制的 return buf.slice(0) } else { //新建填充模式复制 var view = new Uint8Array(buf.byteLength) view.set(new Uint8Array(buf)) return view.buffer } } //方法参考:https://developer.mozilla.org/en-US/docs/Web/API/Body function Body() { this.bodyUsed = false this._initBody = function (body) { // 把最原始的数据存下来 this._bodyInit = body // 判断body数据类型,然后存下来 if (!body) { this._bodyText = '' } else if (typeof body === 'string') { this._bodyText = body } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { this._bodyBlob = body } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { this._bodyFormData = body } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { this._bodyText = body.toString() //数据格式是这样的 a=1&b=2&c=3 } else if (support.arrayBuffer && support.blob && isDataView(body)) { // ArrayBuffer一般是通过DataView或者各种Float32Array,Uint8Array来操作的, https://hacks.mozilla.org/2017/01/typedarray-or-dataview-understanding-byte-order/ // 如果是DataView, DataView的数据是存在 DataView.buffer上的 this._bodyArrayBuffer = bufferClone(body.buffer) // 复制ArrayBuffer // IE 10-11 can't handle a DataView body. this._bodyInit = new Blob([this._bodyArrayBuffer]) // 重新设置_bodyInt } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { // ArrayBuffer一般是通过DataView或者各种Float32Array,Uint8Array来操作的, // https://hacks.mozilla.org/2017/01/typedarray-or-dataview-understanding-byte-order/ this._bodyArrayBuffer = bufferClone(body) } else { throw new Error('unsupported BodyInit type') } // 设置content-type if (!this.headers.get('content-type')) { if (typeof body === 'string') { this.headers.set('content-type', 'text/plain;charset=UTF-8') } else if (this._bodyBlob && this._bodyBlob.type) { this.headers.set('content-type', this._bodyBlob.type) } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') } } } if (support.blob) { // 使用 fetch(...).then(res=>res.blob()) this.blob = function () { //标记为已经使用 var rejected = consumed(this) if (rejected) { return rejected } // resolve,进入then if (this._bodyBlob) { return Promise.resolve(this._bodyBlob) } else if (this._bodyArrayBuffer) { return Promise.resolve(new Blob([this._bodyArrayBuffer])) } else if (this._bodyFormData) { throw new Error('could not read FormData body as blob') } else { return Promise.resolve(new Blob([this._bodyText])) } } // 使用 fetch(...).then(res=>res.arrayBuffer()) this.arrayBuffer = function () { if (this._bodyArrayBuffer) { return consumed(this) || Promise.resolve(this._bodyArrayBuffer) } else { return this.blob().then(readBlobAsArrayBuffer) //如果有blob,读取成ArrayBuffer } } } // 使用 fetch(...).then(res=>res.text()) this.text = function () { var rejected = consumed(this) if (rejected) { return rejected } if (this._bodyBlob) { return readBlobAsText(this._bodyBlob) } else if (this._bodyArrayBuffer) { return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) } else if (this._bodyFormData) { throw new Error('could not read FormData body as text') } else { return Promise.resolve(this._bodyText) } } // 使用 fetch(...).then(res=>res.formData()) if (support.formData) { this.formData = function () { return this.text().then(decode) } } // 使用 fetch(...).then(res=>res.json()) this.json = function () { return this.text().then(JSON.parse) } return this } // HTTP methods whose capitalization should be normalized var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] // 方法名大写 function normalizeMethod(method) { var upcased = method.toUpperCase() return (methods.indexOf(upcased) > -1) ? upcased : method } // 请求的Request对象 ,https://developer.mozilla.org/en-US/docs/Web/API/Request // cache,context,integrity,redirect,referrerPolicy 在MDN定义中是存在的 function Request(input, options) { options = options || {} var body = options.body //如果已经是Request的实例,解析赋值 if (input instanceof Request) { if (input.bodyUsed) { throw new TypeError('Already read') } this.url = input.url //请求的地址 this.credentials = input.credentials //登陆凭证 if (!options.headers) { //headers this.headers = new Headers(input.headers) } this.method = input.method //请求方法 GET,POST...... this.mode = input.mode // same-origin,cors,no-cors if (!body && input._bodyInit != null) { //标记Request已经使用 body = input._bodyInit input.bodyUsed = true } } else { this.url = String(input) } this.credentials = options.credentials || this.credentials || 'omit' if (options.headers || !this.headers) { this.headers = new Headers(options.headers) } this.method = normalizeMethod(options.method || this.method || 'GET') this.mode = options.mode || this.mode || null //same-origin,cors,no-cors this.referrer = null if ((this.method === 'GET' || this.method === 'HEAD') && body) { throw new TypeError('Body not allowed for GET or HEAD requests') } this._initBody(body) //解析值 和设置content-type } // 克隆 Request.prototype.clone = function () { return new Request(this, { body: this._bodyInit }) } // body存为 FormData function decode(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 form } // 用于接续 xhr.getAllResponseHeaders, 数据格式 //Cache-control: private //Content-length:554 function parseHeaders(rawHeaders) { var headers = new Headers() // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space // https://tools.ietf.org/html/rfc7230#section-3.2 var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ') preProcessedHeaders.split(/\r?\n/).forEach(function (line) { var parts = line.split(':') var key = parts.shift().trim() if (key) { var value = parts.join(':').trim() headers.append(key, value) } }) return headers } Body.call(Request.prototype) //把Body方法属性绑到 Reques.prototype // Reponse对象,https://developer.mozilla.org/en-US/docs/Web/API/Response function Response(bodyInit, options) { if (!options) { options = {} } this.type = 'default' this.status = options.status === undefined ? 200 : options.status this.ok = this.status >= 200 && this.status < 300 // 200 - 300 ,https://developer.mozilla.org/en-US/docs/Web/API/Response/ok this.statusText = 'statusText' in options ? options.statusText : 'OK' this.headers = new Headers(options.headers) this.url = options.url || '' this._initBody(bodyInit) // 解析值和设置content-type } Body.call(Response.prototype) //把Body方法属性绑到 Reques.prototype // 克隆Response Response.prototype.clone = function () { return new Response(this._bodyInit, { status: this.status, statusText: this.statusText, headers: new Headers(this.headers), url: this.url }) } //返回一个 error性质的Response,静态方法 Response.error = function () { var response = new Response(null, { status: 0, statusText: '' }) response.type = 'error' return response } var redirectStatuses = [301, 302, 303, 307, 308] // 重定向,本身并不产生实际的效果,静态方法,https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect Response.redirect = function (url, status) { if (redirectStatuses.indexOf(status) === -1) { throw new RangeError('Invalid status code') } return new Response(null, { status: status, headers: { location: url } }) } self.Headers = Headers //暴露Headers self.Request = Request //暴露Request self.Response = Response //暴露Response self.fetch = function (input, init) { return new Promise(function (resolve, reject) { var request = new Request(input, init) //初始化request对象 var xhr = new XMLHttpRequest() // 初始化 xhr xhr.onload = function () { //请求成功,构建Response,并resolve进入下一阶段 var options = { status: xhr.status, statusText: xhr.statusText, headers: parseHeaders(xhr.getAllResponseHeaders() || '') } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') var body = 'response' in xhr ? xhr.response : xhr.responseText resolve(new Response(body, options)) } //请求失败,构建Error,并reject进入下一阶段 xhr.onerror = function () { reject(new TypeError('Network request failed')) } //请求超时,构建Error,并reject进入下一阶段 xhr.ontimeout = function () { reject(new TypeError('Network request failed')) } // 设置xhr参数 xhr.open(request.method, request.url, true) // 设置 credentials if (request.credentials === 'include') { xhr.withCredentials = true } else if (request.credentials === 'omit') { xhr.withCredentials = false } // 设置 responseType if ('responseType' in xhr && support.blob) { xhr.responseType = 'blob' } // 设置Header request.headers.forEach(function (value, name) { xhr.setRequestHeader(name, value) }) // 发送请求 xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } //标记是fetch是polyfill的,而不是原生的 self.fetch.polyfill = true })(typeof self !== 'undefined' ? self : this); // IIFE函数的参数,不用window,web worker, service worker里面也可以使用
小结:
- 可以看出,有些属性是没有实现的,但是一般的请求足以
- Response.body 这种ReadableStream没有实现,自然就没有fetch原生处理progress的方法
fetch('/').then(response => { // response.body is a readable stream. // Calling getReader() gives us exclusive access to the stream's content var reader = response.body.getReader(); var bytesReceived = 0; // read() returns a promise that resolves when a value has been received reader.read().then(function processResult(result) { // Result objects contain two properties: // done - true if the stream has already given you all its data. // value - some data. Always undefined when done is true. if (result.done) { console.log("Fetch complete"); return; } // result.value for fetch streams is a Uint8Array bytesReceived += result.value.length; console.log('Received', bytesReceived, 'bytes of data so far'); // Read some more, and call this function again return reader.read().then(processResult); }); });
参考: