diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 00000000..dbd6bd3c --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,24 @@ +1.5.0 / 2017-02-28 +================== +* Fix self + +1.4.3 / 2016-06-22 +================== +* Compatibility when defining the global object, merged #5 + +1.4.2 / 2016-03-13 +================== +* Fix request and response content-type different bug + +1.4.1 / 2016-01-20 +================== +* Make blob supporting different encoding, fixed #1 + +1.4.0 / 2015-12-09 +================== +* Sync with github/fetch +* Fix promise resolve twice bug + +1.3.1 / 2015-09-08 +================== +* Use console.warn to log messages diff --git a/README.md b/README.md index 445535ca..e5ac6dc9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # window.fetch polyfill -**This fork supports IE8 with es5-shim, es5-sham and es6-promise** +**This fork supports IE8 with es5-shim, es5-sham and es6-promise.** + +**If you also use JSONP, checkout [fetch-jsonp](https://github.com/camsong/fetch-jsonp).** + +**Fetch API is still very new and not fully supported in some browsers, so you may +need to check browser verson as well as if `window.fetch` exists. In this case, +you can set `window.__disableNativeFetch = true` to use AJAX polyfill always.** The global `fetch` function is an easier way to make web requests and handle responses than using an XMLHttpRequest. This polyfill is written as closely as @@ -8,22 +14,19 @@ possible to the standard Fetch specification at https://fetch.spec.whatwg.org. ## Installation -Available on [Bower](http://bower.io) as **fetch-polyfill**. - ```sh -$ bower install fetch-polyfill +$ npm install fetch-ie8 --save ``` You'll also need a Promise polyfill for older browsers. ```sh -$ bower install es6-promise +$ npm install es6-promise ``` -This can also be installed with `npm`. - -```sh -$ npm install fetch-polyfill --save +Run this to polyfill the global environment at the beginning of your application. +```js +require('es6-promise').polyfill(); ``` (For a node.js implementation, try [node-fetch](https://github.com/bitinn/node-fetch)) @@ -157,6 +160,6 @@ header hack. ## Browser Support -![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/src/archive/internet-explorer_7-8/internet-explorer_7-8_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/src/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/src/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 8+ ✔ | Latest ✔ | 6.1+ ✔ | diff --git a/bower.json b/bower.json index 7a2d5b8e..b402f38d 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { - "name": "fetch-polyfill", - "version": "0.8.1", + "name": "fetch-ie8", + "version": "1.3.1", "main": "fetch.js", "devDependencies": { "es6-promise": "2.1.0" diff --git a/fetch.js b/fetch.js index 6aff4092..3791e480 100644 --- a/fetch.js +++ b/fetch.js @@ -1,13 +1,15 @@ -(function() { +(function(self) { 'use strict'; - if (self.fetch) { + // if __disableNativeFetch is set to true, the it will always polyfill fetch + // with Ajax. + if (!self.__disableNativeFetch && self.fetch) { return } function normalizeName(name) { if (typeof name !== 'string') { - name = name.toString(); + name = String(name) } if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { throw new TypeError('Invalid character in header field name') @@ -17,7 +19,7 @@ function normalizeValue(value) { if (typeof value !== 'string') { - value = value.toString(); + value = String(value) } return value } @@ -25,18 +27,15 @@ function Headers(headers) { this.map = {} - var self = this if (headers instanceof Headers) { - headers.forEach(function(name, values) { - values.forEach(function(value) { - self.append(name, value) - }) - }) + headers.forEach(function(value, name) { + this.append(name, value) + }, this) } else if (headers) { Object.getOwnPropertyNames(headers).forEach(function(name) { - self.append(name, headers[name]) - }) + this.append(name, headers[name]) + }, this) } } @@ -72,23 +71,23 @@ this.map[normalizeName(name)] = [normalizeValue(value)] } - // Instead of iterable for now. - Headers.prototype.forEach = function(callback) { - var self = this + Headers.prototype.forEach = function(callback, thisArg) { Object.getOwnPropertyNames(this.map).forEach(function(name) { - callback(name, self.map[name]) - }) + this.map[name].forEach(function(value) { + callback.call(thisArg, value, name, this) + }, this) + }, this) } function consumed(body) { if (body.bodyUsed) { - return fetch.Promise.reject(new TypeError('Already read')) + return Promise.reject(new TypeError('Already read')) } body.bodyUsed = true } function fileReaderReady(reader) { - return new fetch.Promise(function(resolve, reject) { + return new Promise(function(resolve, reject) { reader.onload = function() { resolve(reader.result) } @@ -104,9 +103,18 @@ return fileReaderReady(reader) } - function readBlobAsText(blob) { + function readBlobAsText(blob, options) { var reader = new FileReader() - reader.readAsText(blob) + var contentType = options.headers.map['content-type'] ? options.headers.map['content-type'].toString() : '' + var regex = /charset\=[0-9a-zA-Z\-\_]*;?/ + var _charset = blob.type.match(regex) || contentType.match(regex) + var args = [blob] + + if(_charset) { + args.push(_charset[0].replace(/^charset\=/, '').replace(/;$/, '')) + } + + reader.readAsText.apply(reader, args) return fileReaderReady(reader) } @@ -119,23 +127,28 @@ return false } })(), - formData: 'FormData' in self + formData: 'FormData' in self, + arrayBuffer: 'ArrayBuffer' in self } function Body() { this.bodyUsed = false - this._initBody = function(body) { + this._initBody = function(body, options) { this._bodyInit = body if (typeof body === 'string') { this._bodyText = body } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { this._bodyBlob = body + this._options = options } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { this._bodyFormData = body } else if (!body) { this._bodyText = '' + } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) { + // Only support ArrayBuffers for POST method. + // Receiving ArrayBuffers happens via Blobs, instead. } else { throw new Error('unsupported BodyInit type') } @@ -149,11 +162,11 @@ } if (this._bodyBlob) { - return fetch.Promise.resolve(this._bodyBlob) + return Promise.resolve(this._bodyBlob) } else if (this._bodyFormData) { throw new Error('could not read FormData body as blob') } else { - return fetch.Promise.resolve(new Blob([this._bodyText])) + return Promise.resolve(new Blob([this._bodyText])) } } @@ -168,17 +181,17 @@ } if (this._bodyBlob) { - return readBlobAsText(this._bodyBlob) + return readBlobAsText(this._bodyBlob, this._options) } else if (this._bodyFormData) { throw new Error('could not read FormData body as text') } else { - return fetch.Promise.resolve(this._bodyText) + return Promise.resolve(this._bodyText) } } } else { this.text = function() { var rejected = consumed(this) - return rejected ? rejected : fetch.Promise.resolve(this._bodyText) + return rejected ? rejected : Promise.resolve(this._bodyText) } } @@ -189,9 +202,7 @@ } this.json = function() { - return this.text().then(function (text) { - return JSON.parse(text); - }); + return this.text().then(JSON.parse) } return this @@ -205,20 +216,44 @@ return (methods.indexOf(upcased) > -1) ? upcased : method } - function Request(url, options) { + function Request(input, options) { options = options || {} - this.url = url + var body = options.body + if (Request.prototype.isPrototypeOf(input)) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url + this.credentials = input.credentials + if (!options.headers) { + this.headers = new Headers(input.headers) + } + this.method = input.method + this.mode = input.mode + if (!body) { + body = input._bodyInit + input.bodyUsed = true + } + } else { + this.url = input + } - this.credentials = options.credentials || 'omit' - this.headers = new Headers(options.headers) - this.method = normalizeMethod(options.method || 'GET') - this.mode = options.mode || null + 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 this.referrer = null - if ((this.method === 'GET' || this.method === 'HEAD') && options.body) { + if ((this.method === 'GET' || this.method === 'HEAD') && body) { throw new TypeError('Body not allowed for GET or HEAD requests') } - this._initBody(options.body) + this._initBody(body, options) + } + + Request.prototype.clone = function() { + return new Request(this) } function decode(body) { @@ -246,20 +281,6 @@ return head } - var noXhrPatch = - typeof window !== 'undefined' && !!window.ActiveXObject && - !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); - - function getXhr() { - // from backbone.js 1.1.2 - // https://github.com/jashkenas/backbone/blob/1.1.2/backbone.js#L1181 - if (noXhrPatch && !(/^(get|post|head|put|delete|options)$/i.test(this.method))) { - this.usingActiveXhr = true; - return new ActiveXObject("Microsoft.XMLHTTP"); - } - return new XMLHttpRequest(); - } - Body.call(Request.prototype) function Response(bodyInit, options) { @@ -267,9 +288,8 @@ options = {} } - this._initBody(bodyInit) + this._initBody(bodyInit, options) this.type = 'default' - this.url = null this.status = options.status this.ok = this.status >= 200 && this.status < 300 this.statusText = options.statusText @@ -279,25 +299,46 @@ Body.call(Response.prototype) + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + } + + Response.error = function() { + var response = new Response(null, {status: 0, statusText: ''}) + response.type = 'error' + return response + } + + var redirectStatuses = [301, 302, 303, 307, 308] + + 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; self.Request = Request; self.Response = Response; self.fetch = function(input, init) { - // TODO: Request constructor should accept input, init - var request - if (Request.prototype.isPrototypeOf(input) && !init) { - request = input - } else { - request = new Request(input, init) - } - - return new fetch.Promise(function(resolve, reject) { - var xhr = getXhr(); - if (request.credentials === 'cors') { - xhr.withCredentials = true; + return new Promise(function(resolve, reject) { + var request + if (Request.prototype.isPrototypeOf(input) && !init) { + request = input + } else { + request = new Request(input, init) } + var xhr = new XMLHttpRequest() + function responseURL() { if ('responseURL' in xhr) { return xhr.responseURL @@ -311,12 +352,15 @@ return; } + var __onLoadHandled = false; + function onload() { if (xhr.readyState !== 4) { return } var status = (xhr.status === 1223) ? 204 : xhr.status if (status < 100 || status > 599) { + if (__onLoadHandled) { return; } else { __onLoadHandled = true; } reject(new TypeError('Network request failed')) return } @@ -327,31 +371,48 @@ url: responseURL() } var body = 'response' in xhr ? xhr.response : xhr.responseText; + + if (__onLoadHandled) { return; } else { __onLoadHandled = true; } resolve(new Response(body, options)) } xhr.onreadystatechange = onload; - if (!self.usingActiveXhr) { - xhr.onload = onload; - xhr.onerror = function() { - reject(new TypeError('Network request failed')) - } + xhr.onload = onload; + xhr.onerror = function() { + if (__onLoadHandled) { return; } else { __onLoadHandled = true; } + reject(new TypeError('Network request failed')) } xhr.open(request.method, request.url, true) + // `withCredentials` should be setted after calling `.open` in IE10 + // http://stackoverflow.com/a/19667959/1219343 + try { + if (request.credentials === 'include') { + if ('withCredentials' in xhr) { + xhr.withCredentials = true; + } else { + console && console.warn && console.warn('withCredentials is not supported, you can ignore this warning'); + } + } + } catch (e) { + console && console.warn && console.warn('set withCredentials error:' + e); + } + if ('responseType' in xhr && support.blob) { xhr.responseType = 'blob' } - request.headers.forEach(function(name, values) { - values.forEach(function(value) { - xhr.setRequestHeader(name, value) - }) + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value) }) xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } - fetch.Promise = self.Promise; // you could change it to your favorite alternative self.fetch.polyfill = true -})(); + + // Support CommonJS + if (typeof module !== 'undefined' && module.exports) { + module.exports = self.fetch; + } +})(typeof self !== 'undefined' ? self : this); diff --git a/package.json b/package.json index 47fd4c51..203b141d 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,9 @@ { - "name": "fetch-polyfill", - "version": "0.8.1", + "name": "fetch-ie8", + "version": "1.5.0", "main": "fetch.js", - "repository": "undoZen/fetch", - "licenses": [ - { - "type": "MIT", - "url": "http://mit-license.org/undozen" - } - ], + "repository": "camsong/fetch", + "license": "MIT", "devDependencies": { "browserify": "^9.0.8", "es6-promise": "^2.1.1",