├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── dist └── fetcher.js ├── fetcher.es6.js ├── fetcher.js ├── fetcher.node.js ├── make.js ├── package.json ├── param.js └── test ├── index.html └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | } 5 | "ecmaFeatures": { 6 | "modules": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 othree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: param.js fetcher.js 2 | cat param.js fetcher.js > dist/fetcher.js 3 | 4 | param.js: 5 | cat node_modules/jquery-param/src/jquery-param.js | dos2unix > param.js 6 | 7 | fetcher.js: fetcher.es6.js 8 | babel --modules umd --module-id fetcher fetcher.es6.js > fetcher.js 9 | 10 | node_modules/jquery-param/src/jquery-param.js: 11 | npm install 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fetch-er 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/othree/fetcher.svg?branch=master)](https://travis-ci.org/othree/fetcher) 5 | 6 | A WHATWG [fetch][] helper. WHATWG's fetch is very modern. Simple name, uses Promise, options object. 7 | But it designed as a low level API. Developers have to deal with some detail. ex: Post parameter 8 | serialize, transform response JSON to JavaScript object. So here is the fetch-er to help you deal 9 | with these stuff. Inspired by jQuery.ajax. 10 | 11 | 12 | [fetch]:http://updates.html5rocks.com/2015/03/introduction-to-fetch 13 | 14 | Example 15 | ------- 16 | 17 | ```javascript 18 | fetcher.get('/api/users', null, {dataType: 'json'}).then( ([value, res]) => { 19 | //... 20 | } ) 21 | 22 | fetcher.getJSON('/api/users').then( ... ) 23 | 24 | fetcher.post('/api/users', {name: 'John'}).then( ... ) 25 | 26 | fetcher.put('/api/users', {name: 'Wick'}).then( ... ) 27 | 28 | fetcher.delete('/api/users/23').then( ... ) 29 | ``` 30 | 31 | fetch-er is Not 32 | --------------- 33 | 34 | fetch-er is not designed for every case. If you belongs to one of following situation. You should not use fetch-er: 35 | 36 | * Feeling good to use fetch API. Since it's not hard to use fetch without helper. 37 | * Every byte count, you need to keep your script as small as possible. Every byte matters. 38 | 39 | Doc 40 | --- 41 | 42 | fetch-er provide a new global object for browser environment, called `fetcher`. `fetcher` is an instance 43 | of private `Fetcher` class. In other module systems like: CommonJS, AMD, NodeJS. You will get the same 44 | instance of Fetcher when you include this module. 45 | 46 | ```javascript 47 | var fetcher = require('fetch-er') 48 | ``` 49 | 50 | To install, you can use `npm` or `bower` or just download `dist/fetcher.js`. 51 | 52 | ```shell 53 | npm i fetch-er 54 | bower i fetch-er 55 | ``` 56 | 57 | 58 | The Fetcher class have the following basic methods: `delete`, `get`, `getJSON`, `head`, `options`, `post` 59 | and `put`. Mapping the method name to HTTP method for the method will use. All methods receives three 60 | arguments: 61 | 62 | * `url`: The url location of the request 63 | * `data`: Optional data for the request 64 | * `options`: Optional options object send to fetch 65 | 66 | The options object will send to `fetch` and fetcher provides several new options: 67 | 68 | * `contentType`: The data type of request body you are going to send. Will overwrite the one in headers. 69 | * `dataType`: The data type you expect to receive from server. Supports mime type and following shorcut 70 | `json`, `text` and `xml`. 71 | * `mimeType`: Will overwrite response mimeType before parse to data. 72 | * `timeout`: Will reject returned promise when time limit reach, but no actual abort now(current fetch don't have abort). 73 | 74 | 75 | What fetcher will do when you do a request through it: 76 | 77 | 1. If method is `GET` or `HEAD`, parse data to form-urlencoded form. Append to request url. 78 | 2. Auto generate request body if necessary. (json, form-urlencoded) 79 | * JSON, if a request contains headers have `Content-Type: application/json` or `options.contentType` with the same value. 80 | The data will parsed by `JSON.stringify` and write to body. 81 | * FormData or ArrayBuffer will send to fetch directly. 82 | * Default request body is `form-urlencoded`, use [jquery-param](https://www.npmjs.com/package/jquery-param). 83 | 3. Set mode to `cors` if request to a different hostname. 84 | 4. Auto parse response data. Fetcher will try to figure out what to do based on response content type and `options.dataType`. 85 | * JSON string will parsed by `JSON.parse`. 86 | * HTML will be plain text. If you want DOM node as response. You can set `options.dataType` to `xml`. 87 | * XML will be parse by `DOMParser`. 88 | * ArrayBuffer or FormData will only available if user set `options.dataType`. 89 | * Otherwise, response will be plain text. 90 | 91 | Fetcher methods will return a Promise just like fetch. But it will be fulfilled with different value, an 92 | array(`[value, status, response]`). First element is the response value. Second element is text response status. Possible status: 93 | 94 | * `nocontent` for 200 or HEAD request. 95 | * `notmodified` for 304 not modified. 96 | * `success` for other success request. 97 | 98 | Third element is consumed response object. The reason to use array is easier to use ES6 destructuring assign. Ex: 99 | 100 | ```javascript 101 | fetcher.get('/api').then( ([value, status, response]) => { 102 | // blah... 103 | }) 104 | ``` 105 | 106 | PS. Plan to return not consumed response. But current polyfill don't support clone. 107 | 108 | #### request(method, url, data, options) 109 | 110 | There is one more method called `request`. Is the base of all other methods. Receive four arguments: `method`, 111 | `url`, `data` and `options`. The method is in string format. All uppercase characters. Which will pass to 112 | fetch directly. And fetch will check is method valid. 113 | 114 | If an error happened on fetcher reqeust. The returned promise will reject just like a normal fetch request. 115 | This only happens when response status is not normal (100 to 599) or network error. By design fetch will fulfill 116 | returned Promise when server have response. And developers can use `response.ok` to check is this request success. 117 | Only when status code between 200 to 299 will set `ok` to true. But jQuery also accept `304` not modified. 118 | And jQuery will reject all other status code. The behavior is very different. And fetcher still not decide which 119 | to follow. 120 | 121 | The rejected promise will use an array to reject(`[error, response]`). Some error will not get response. 122 | Ex: timeout or network error. 123 | 124 | #### setup(options) 125 | 126 | There is a method called `setup` used for setup default option. The default option will be used on every request. 127 | But possible to overwrite when make the request. Current supported options are `method`, `contentType`, `dataType`, 128 | `mimeType`, `timeout` and `converters`. Default global options are: 129 | 130 | ```javascript 131 | { 132 | method: 'get', 133 | converters: { 134 | 'text json': JSON.parse, 135 | 'text xml': parseXML 136 | } 137 | } 138 | ``` 139 | 140 | 141 | ### Compare to jQuery.ajax 142 | 143 | Stat: `y`: support, `p`: partial, `n`: not support, `n/a`: not possible, `todo`: in plan. 144 | 145 | | Feature | Stat | 146 | |-----------------|-------------------| 147 | | accepts | y | 148 | | ajaxPrefilter() | n | 149 | | ajaxSetup() | p, use setup() | 150 | | ajaxTransport() | n/a | 151 | | async | n/a | 152 | | beforeSend | n | 153 | | cache | n | 154 | | complete | use promise chain | 155 | | contents | n/a | 156 | | contentType | y | 157 | | context | n/a | 158 | | converters | y, 2 level only | 159 | | crossDomain | auto | 160 | | data | y | 161 | | dataFilter | n | 162 | | dataType | y | 163 | | error | use promise chain | 164 | | global | n/a | 165 | | headers | y | 166 | | ifModified | n | 167 | | isLocal | n | 168 | | jsonp | n | 169 | | jsonpCallback | n | 170 | | method | y | 171 | | mimeType | y | 172 | | password | n/a | 173 | | processData | todo | 174 | | scriptCharset | n | 175 | | statusCode | n | 176 | | success | use promise chain | 177 | | timeout | y | 178 | | traditional | n/a, based on dep | 179 | | type | y | 180 | | url | y | 181 | | username | n/a | 182 | | xhr | n/a | 183 | | xhrFields | n/a | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-er", 3 | "main": "dist/fetcher.js", 4 | "version": "0.0.8", 5 | "homepage": "https://github.com/othree/fetcher", 6 | "authors": [ 7 | "othree " 8 | ], 9 | "description": "WHATWG fetch helper, inspired by jQuery.ajax", 10 | "keywords": [ 11 | "fetch" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "node_modules", 16 | "package.json", 17 | "test/", 18 | "fetcher.es6.js", 19 | "fetcher.js", 20 | "param.js", 21 | "Makefile", 22 | "*.md" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /dist/fetcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT 3 | */ 4 | /*jslint forin: true, plusplus: true */ 5 | /*global module, define */ 6 | (function (global) { 7 | 'use strict'; 8 | 9 | var param = function (a) { 10 | var add = function (s, k, v) { 11 | v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; 12 | s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v); 13 | }, buildParams = function (prefix, obj, s) { 14 | var i, len, key; 15 | 16 | if (Object.prototype.toString.call(obj) === '[object Array]') { 17 | for (i = 0, len = obj.length; i < len; i++) { 18 | buildParams(prefix + '[' + (typeof obj[i] === 'object' ? i : '') + ']', obj[i], s); 19 | } 20 | } else if (obj && obj.toString() === '[object Object]') { 21 | for (key in obj) { 22 | if (obj.hasOwnProperty(key)) { 23 | if (prefix) { 24 | buildParams(prefix + '[' + key + ']', obj[key], s, add); 25 | } else { 26 | buildParams(key, obj[key], s, add); 27 | } 28 | } 29 | } 30 | } else if (prefix) { 31 | add(s, prefix, obj); 32 | } else { 33 | for (key in obj) { 34 | add(s, key, obj[key]); 35 | } 36 | } 37 | return s; 38 | }; 39 | return buildParams('', a, []).join('&').replace(/%20/g, '+'); 40 | }; 41 | 42 | if (typeof module === 'object' && typeof module.exports === 'object') { 43 | module.exports = param; 44 | } else if (typeof define === 'function' && define.amd) { 45 | define([], function () { 46 | return param; 47 | }); 48 | } else { 49 | global.param = param; 50 | } 51 | 52 | }(this)); 53 | (function (global, factory) { 54 | if (typeof define === 'function' && define.amd) { 55 | define('fetcher', ['exports', 'module', 'jquery-param'], factory); 56 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { 57 | factory(exports, module, require('jquery-param')); 58 | } else { 59 | var mod = { 60 | exports: {} 61 | }; 62 | factory(mod.exports, mod, global._param); 63 | global.fetcher = mod.exports; 64 | } 65 | })(this, function (exports, module, _jqueryParam) { 66 | 'use strict'; 67 | 68 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 69 | 70 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 71 | 72 | function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } 73 | 74 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 75 | 76 | var _param2 = _interopRequireDefault(_jqueryParam); 77 | 78 | var g = undefined; 79 | if (typeof self !== 'undefined') { 80 | g = self; 81 | } 82 | if (typeof global !== 'undefined') { 83 | g = global; 84 | } 85 | if (typeof window !== 'undefined') { 86 | g = window; 87 | } 88 | 89 | // https://github.com/jquery/jquery/blob/master/src/ajax.js#L20 90 | var rnoContent = /^(?:GET|HEAD)$/; 91 | 92 | // https://github.com/github/fetch/blob/master/fetch.js#L113 93 | var support = { 94 | blob: 'FileReader' in g && 'Blob' in g && (function () { 95 | try { 96 | new Blob(); 97 | return true; 98 | } catch (e) { 99 | return false; 100 | } 101 | })(), 102 | formData: 'FormData' in g 103 | }; 104 | 105 | var parseXML = function parseXML(text, mimeType) { 106 | if (g) { 107 | // in browser 108 | // https://github.com/jquery/jquery/blob/master/src/ajax/parseXML.js 109 | try { 110 | xml = new g.DOMParser().parseFromString(text, mime); 111 | } catch (e) { 112 | xml = undefined; 113 | } 114 | if (!xml || xml.getElementsByTagName('parsererror').length) { 115 | throw new Error('Invalid XML: ' + text); 116 | } 117 | } else { 118 | // node, return plain text 119 | xml = text; 120 | } 121 | return xml; 122 | }; 123 | 124 | var resXML = function resXML(res, mimeType) { 125 | var xml; 126 | var type = mimeType; 127 | var mime = type ? type.split(';').unshift() : 'text/xml'; 128 | var text = res.text(); 129 | return text.then(function (text) { 130 | return parseXML(text, mime); 131 | }); 132 | }; 133 | 134 | var resText = function resText(res) { 135 | return res.text(); 136 | }; 137 | 138 | var resTractors = { 139 | arraybuffer: function arraybuffer(res) { 140 | return res.arrayBuffer(); 141 | }, 142 | blob: function blob(res) { 143 | return res.blob(); 144 | }, 145 | formdata: function formdata(res) { 146 | return res.formData(); 147 | }, 148 | html: resText, 149 | json: function json(res) { 150 | return res.json(); 151 | }, 152 | plain: resText, 153 | text: resText, 154 | xml: resXML 155 | }; 156 | 157 | var isCORS = function isCORS(url) { 158 | if (g.document && g.document.location && /^\w+:\/\//.test(url)) { 159 | var frags = url.replace(/^\w+:\/\//, ''); 160 | var index = url.indexOf('/'); 161 | var hostname = frags.substr(0, index); 162 | return hostname !== g.document.location.hostname; 163 | } 164 | return false; 165 | }; 166 | 167 | var shortContentType = { 168 | '*': '*/*', 169 | json: 'application/json', 170 | text: 'text/plain', 171 | xml: 'application/xml' 172 | }; 173 | 174 | var normalizeContentType = function normalizeContentType(contentType) { 175 | return shortContentType[contentType] || contentType; 176 | }; 177 | 178 | var Fetcher = (function () { 179 | function Fetcher() { 180 | _classCallCheck(this, Fetcher); 181 | 182 | this.options = { 183 | method: 'get', 184 | converters: { 185 | 'text json': JSON.parse, 186 | 'text xml': parseXML 187 | } 188 | }; 189 | } 190 | 191 | _createClass(Fetcher, [{ 192 | key: 'param', 193 | value: function param(data) { 194 | return (0, _param2['default'])(data); 195 | } 196 | }, { 197 | key: 'setup', 198 | value: function setup(options) { 199 | for (var k in options) { 200 | var v = options[k]; 201 | if (typeof v === 'object') { 202 | for (var kk in options) { 203 | var vv = options[kk]; 204 | this.options[k][kk] = vv; 205 | } 206 | } else { 207 | this.options[k] = v; 208 | } 209 | } 210 | } 211 | }, { 212 | key: 'request', 213 | value: function request(method, url, data) { 214 | var _this = this; 215 | 216 | var options = arguments[3] === undefined ? {} : arguments[3]; 217 | 218 | var m = method || options.method || options.type || this.options.method || 'get'; 219 | options.method = m.trim().toUpperCase(); 220 | 221 | if (options.headers && options.headers['Content-Type']) { 222 | options.headers['Content-Type'] = normalizeContentType(options.headers['Content-Type']); 223 | } 224 | if (options.contentType) { 225 | options.contentType = normalizeContentType(options.contentType); 226 | } 227 | var headers = new Headers(options.headers || {}); 228 | options.headers = headers; 229 | 230 | // auto set to cors if hotname is different 231 | if (!options.mode && isCORS(url)) { 232 | options.mode = 'cors'; 233 | } 234 | 235 | if (rnoContent.test(options.method)) { 236 | // set query parameter got GET/HEAD 237 | var query = this.param(data); 238 | if (query) { 239 | url = url + (/\?/.test(url) ? '&' : '?') + query; 240 | } 241 | } else { 242 | // Other method will have request body 243 | 244 | // grab and delete Content-Type header 245 | // fetch will set Content-Type for common cases 246 | var contentType = options.contentType || this.options.contentType || headers.get('Content-Type'); 247 | headers['delete']('Content-Type'); 248 | 249 | // set body 250 | if (typeof data === 'string' || support.formdata && FormData.prototype.isPrototypeOf(data) || support.blob && Blob.prototype.isPrototypeOf(data)) { 251 | if (contentType) { 252 | headers.set('Content-Type', contentType); 253 | } 254 | options.body = data; 255 | } else if (contentType === 'application/json') { 256 | headers.set('Content-Type', contentType); 257 | options.body = JSON.stringify(data); 258 | } else if (data) { 259 | // x-www-form-urlencoded is default in fetch 260 | options.body = this.param(data); 261 | } 262 | } 263 | 264 | var extractor; 265 | 266 | var dataType = options.dataType || this.options.dataType || '*'; 267 | dataType = dataType.trim(); 268 | dataType = dataType === '*' ? '' : dataType; 269 | 270 | var mimeType = options.mimeType || this.options.mimeType || ''; 271 | mimeType = mimeType.trim(); 272 | 273 | var accepts = options.accepts || '*/*'; 274 | 275 | if (dataType && shortContentType[dataType]) { 276 | accepts = shortContentType[dataType]; 277 | if (dataType !== '*') { 278 | accepts += ', ' + shortContentType['*'] + '; q=0.01'; 279 | } 280 | } 281 | 282 | delete options.dataType; 283 | delete options.mimeType; 284 | delete options.accepts; 285 | 286 | headers.set('Accept', accepts); 287 | 288 | var racers = []; 289 | 290 | var timeout = options.timeout || this.options.timeout; 291 | if (typeof timeout === 'number') { 292 | racers.push(new Promise(function (resolve, reject) { 293 | setTimeout(function () { 294 | reject([new Error('timeout')]); 295 | }, timeout); 296 | })); 297 | delete options.timeout; 298 | } 299 | 300 | racers.push(fetch(url, options).then(function (res) { 301 | var statusText = res.statusText; 302 | if (!res.ok && res.status !== 304) { 303 | return Promise.reject([new Error(statusText), res]); 304 | } 305 | 306 | if (res.status === 204 || options.method === 'HEAD') { 307 | // if no content 308 | statusText = 'nocontent'; 309 | } else if (res.status === 304) { 310 | // if not modified 311 | statusText = 'notmodified'; 312 | } else { 313 | statusText = 'success'; 314 | } 315 | 316 | var contentType = res.headers.get('Content-Type') || ''; 317 | var second = function second(value) { 318 | return value; 319 | }; 320 | 321 | mimeType = mimeType || contentType.split(';').shift(); 322 | dataType = mimeType.split(/[\/+]/).pop().toLowerCase() || dataType || 'text'; 323 | extractor = resTractors[dataType]; 324 | 325 | if (!extractor && typeof options.converters === 'object') { 326 | for (var fromto in options.converters) { 327 | var _fromto$split = fromto.split(' '); 328 | 329 | var _fromto$split2 = _slicedToArray(_fromto$split, 2); 330 | 331 | var from = _fromto$split2[0]; 332 | var to = _fromto$split2[1]; 333 | 334 | if (to === dataType && resTractors[from]) { 335 | extractor = resTractors[from]; 336 | second = _this.options.converters[fromto]; 337 | break; 338 | } 339 | } 340 | } 341 | 342 | if (!extractor) { 343 | for (var fromto in _this.options.converters) { 344 | if (options.converters[fromto]) { 345 | continue; 346 | } 347 | 348 | var _fromto$split3 = fromto.split(' '); 349 | 350 | var _fromto$split32 = _slicedToArray(_fromto$split3, 2); 351 | 352 | var from = _fromto$split32[0]; 353 | var to = _fromto$split32[1]; 354 | 355 | if (to === dataType && resTractors[from]) { 356 | extractor = resTractors[from]; 357 | second = _this.options.converters[fromto]; 358 | break; 359 | } 360 | } 361 | } 362 | 363 | var value = extractor ? extractor(res, mimeType).then(second) : Promise.reject(new Error('No converter for response to ' + dataType)); 364 | 365 | return Promise.all([value, statusText, res])['catch'](function (error) { 366 | throw [error, res]; 367 | }); 368 | }, function (error) { 369 | throw [error]; 370 | })); 371 | 372 | return Promise.race(racers); 373 | } 374 | }, { 375 | key: 'delete', 376 | value: function _delete(url, data, options) { 377 | return this.request('DELETE', url, data, options); 378 | } 379 | }, { 380 | key: 'get', 381 | value: function get(url, data, options) { 382 | return this.request('GET', url, data, options); 383 | } 384 | }, { 385 | key: 'getJSON', 386 | value: function getJSON(url, data) { 387 | var options = arguments[2] === undefined ? {} : arguments[2]; 388 | 389 | options.dataType = 'json'; 390 | return this.get(url, data, options); 391 | } 392 | }, { 393 | key: 'head', 394 | value: function head(url, data, options) { 395 | return this.request('HEAD', url, data, options); 396 | } 397 | }, { 398 | key: 'options', 399 | value: function options(url, data, _options) { 400 | return this.request('OPTIONS', url, data, _options); 401 | } 402 | }, { 403 | key: 'post', 404 | value: function post(url, data, options) { 405 | return this.request('POST', url, data, options); 406 | } 407 | }, { 408 | key: 'put', 409 | value: function put(url, data, options) { 410 | return this.request('PUT', url, data, options); 411 | } 412 | }]); 413 | 414 | return Fetcher; 415 | })(); 416 | 417 | ; 418 | 419 | module.exports = new Fetcher(); 420 | }); 421 | 422 | -------------------------------------------------------------------------------- /fetcher.es6.js: -------------------------------------------------------------------------------- 1 | 2 | var g = this; 3 | if (typeof self !== 'undefined') { g = self; } 4 | if (typeof global !== 'undefined') { g = global; } 5 | if (typeof window !== 'undefined') { g = window; } 6 | 7 | import param from 'jquery-param'; 8 | 9 | // https://github.com/jquery/jquery/blob/master/src/ajax.js#L20 10 | var rnoContent = /^(?:GET|HEAD)$/; 11 | 12 | // https://github.com/github/fetch/blob/master/fetch.js#L113 13 | var support = { 14 | blob: 'FileReader' in g && 'Blob' in g && ( () => { 15 | try { 16 | new Blob(); 17 | return true 18 | } catch(e) { 19 | return false 20 | } 21 | })(), 22 | formData: 'FormData' in g 23 | }; 24 | 25 | var parseXML = (text, mimeType) => { 26 | if (g) { 27 | // in browser 28 | // https://github.com/jquery/jquery/blob/master/src/ajax/parseXML.js 29 | try { 30 | xml = ( new g.DOMParser() ).parseFromString( text, mime ); 31 | } catch ( e ) { 32 | xml = undefined; 33 | } 34 | if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { 35 | throw new Error("Invalid XML: " + text ); 36 | } 37 | } else { 38 | // node, return plain text 39 | xml = text; 40 | } 41 | return xml; 42 | } 43 | 44 | var resXML = (res, mimeType) => { 45 | var xml; 46 | var type = mimeType; 47 | var mime = type ? type.split(';').unshift() : 'text/xml' ; 48 | var text = res.text(); 49 | return text.then( text => parseXML(text, mime) ); 50 | } 51 | 52 | var resText = res => res.text() 53 | 54 | var resTractors = { 55 | arraybuffer: res => res.arrayBuffer(), 56 | blob: res => res.blob(), 57 | formdata: res => res.formData(), 58 | html: resText, 59 | json: res => res.json(), 60 | plain: resText, 61 | text: resText, 62 | xml: resXML 63 | }; 64 | 65 | var isCORS = url => { 66 | if (g.document && g.document.location && /^\w+:\/\//.test(url) ) { 67 | var frags = url.replace(/^\w+:\/\//, ''); 68 | var index = url.indexOf('/'); 69 | var hostname = frags.substr(0, index); 70 | return hostname !== g.document.location.hostname; 71 | } 72 | return false; 73 | } 74 | 75 | var shortContentType = { 76 | "*": '*/*', 77 | json: 'application/json', 78 | text: 'text/plain', 79 | xml: 'application/xml' 80 | } 81 | 82 | var normalizeContentType = contentType => shortContentType[contentType] || contentType; 83 | 84 | 85 | 86 | class Fetcher { 87 | constructor() { 88 | this.options = { 89 | method: 'get', 90 | converters: { 91 | 'text json': JSON.parse, 92 | 'text xml': parseXML 93 | } 94 | }; 95 | } 96 | 97 | param(data) { 98 | return param(data); 99 | } 100 | 101 | setup(options) { 102 | for (let k in options) { 103 | let v = options[k]; 104 | if (typeof v === 'object') { 105 | for (let kk in options) { 106 | let vv = options[kk]; 107 | this.options[k][kk] = vv; 108 | } 109 | } else { 110 | this.options[k] = v; 111 | } 112 | } 113 | } 114 | 115 | request(method, url, data, options = {}) { 116 | var m = method || options.method || options.type || this.options.method || 'get'; 117 | options.method = m.trim().toUpperCase(); 118 | 119 | if (options.headers && options.headers["Content-Type"]) { 120 | options.headers["Content-Type"] = normalizeContentType(options.headers["Content-Type"]); 121 | } 122 | if (options.contentType) { 123 | options.contentType = normalizeContentType(options.contentType); 124 | } 125 | var headers = new Headers(options.headers || {}); 126 | options.headers = headers; 127 | 128 | // auto set to cors if hotname is different 129 | if (!options.mode && isCORS(url)) { 130 | options.mode = 'cors'; 131 | } 132 | 133 | if (rnoContent.test(options.method)) { 134 | // set query parameter got GET/HEAD 135 | var query = this.param(data); 136 | if (query) { 137 | url = url + (/\?/.test(url) ? '&' : '?') + query; 138 | } 139 | } else { 140 | // Other method will have request body 141 | 142 | // grab and delete Content-Type header 143 | // fetch will set Content-Type for common cases 144 | var contentType = options.contentType || this.options.contentType || headers.get('Content-Type'); 145 | headers.delete("Content-Type"); 146 | 147 | // set body 148 | if (typeof data === 'string' 149 | || (support.formdata && FormData.prototype.isPrototypeOf(data)) 150 | || (support.blob && Blob.prototype.isPrototypeOf(data)) ) { 151 | if (contentType) { headers.set("Content-Type", contentType); } 152 | options.body = data; 153 | } else if (contentType === 'application/json') { 154 | headers.set("Content-Type", contentType); 155 | options.body = JSON.stringify(data); 156 | } else if (data) { 157 | // x-www-form-urlencoded is default in fetch 158 | options.body = this.param(data); 159 | } 160 | } 161 | 162 | var extractor; 163 | 164 | var dataType = options.dataType || this.options.dataType || '*'; 165 | dataType = dataType.trim(); 166 | dataType = (dataType === '*') ? '' : dataType; 167 | 168 | var mimeType = options.mimeType || this.options.mimeType || ''; 169 | mimeType = mimeType.trim(); 170 | 171 | var accepts = options.accepts || '*/*'; 172 | 173 | if (dataType && shortContentType[dataType]) { 174 | accepts = shortContentType[dataType]; 175 | if (dataType !== '*') { 176 | accepts += ', ' + shortContentType['*'] + '; q=0.01'; 177 | } 178 | } 179 | 180 | delete options.dataType; 181 | delete options.mimeType; 182 | delete options.accepts; 183 | 184 | headers.set('Accept', accepts); 185 | 186 | var racers = []; 187 | 188 | var timeout = options.timeout || this.options.timeout; 189 | if (typeof timeout === 'number') { 190 | racers.push(new Promise(function (resolve, reject) { 191 | setTimeout( () => { reject([new Error('timeout')]) }, timeout); 192 | })); 193 | delete options.timeout; 194 | } 195 | 196 | racers.push(fetch(url, options).then( res => { 197 | var statusText = res.statusText; 198 | if (!res.ok && res.status !== 304) { 199 | return Promise.reject([new Error(statusText), res]); 200 | } 201 | 202 | if ( res.status === 204 || options.method === "HEAD" ) { 203 | // if no content 204 | statusText = "nocontent"; 205 | } else if ( res.status === 304 ) { 206 | // if not modified 207 | statusText = "notmodified"; 208 | } else { 209 | statusText = "success"; 210 | } 211 | 212 | var contentType = res.headers.get('Content-Type') || ''; 213 | var second = value => value; 214 | 215 | mimeType = mimeType || contentType.split(';').shift(); 216 | dataType = mimeType.split(/[\/+]/).pop().toLowerCase() || dataType || 'text'; 217 | extractor = resTractors[dataType]; 218 | 219 | if (!extractor && typeof options.converters === 'object') { 220 | for (let fromto in options.converters) { 221 | let [from, to] = fromto.split(' '); 222 | if (to === dataType && resTractors[from]) { 223 | extractor = resTractors[from]; 224 | second = this.options.converters[fromto]; 225 | break; 226 | } 227 | } 228 | } 229 | 230 | if (!extractor) { 231 | for (let fromto in this.options.converters) { 232 | if (options.converters[fromto]) { continue; } 233 | let [from, to] = fromto.split(' '); 234 | if (to === dataType && resTractors[from]) { 235 | extractor = resTractors[from]; 236 | second = this.options.converters[fromto]; 237 | break; 238 | } 239 | } 240 | } 241 | 242 | var value = extractor ? extractor(res, mimeType).then(second) : Promise.reject(new Error(`No converter for response to ${dataType}`)); 243 | 244 | return Promise.all([value, statusText, res]).catch( error => { throw([error, res]); }); 245 | }, error => { 246 | throw [error]; 247 | })); 248 | 249 | return Promise.race(racers); 250 | } 251 | 252 | delete(url, data, options) { 253 | return this.request('DELETE', url, data, options); 254 | } 255 | get(url, data, options) { 256 | return this.request('GET', url, data, options); 257 | } 258 | getJSON(url, data, options = {}) { 259 | options.dataType = 'json' 260 | return this.get(url, data, options); 261 | } 262 | head(url, data, options) { 263 | return this.request('HEAD', url, data, options); 264 | } 265 | options(url, data, options) { 266 | return this.request('OPTIONS', url, data, options); 267 | } 268 | post(url, data, options) { 269 | return this.request('POST', url, data, options); 270 | } 271 | put(url, data, options) { 272 | return this.request('PUT', url, data, options); 273 | } 274 | }; 275 | 276 | export default new Fetcher(); 277 | -------------------------------------------------------------------------------- /fetcher.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define('fetcher', ['exports', 'module', 'jquery-param'], factory); 4 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { 5 | factory(exports, module, require('jquery-param')); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, mod, global._param); 11 | global.fetcher = mod.exports; 12 | } 13 | })(this, function (exports, module, _jqueryParam) { 14 | 'use strict'; 15 | 16 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 19 | 20 | function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } 21 | 22 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 23 | 24 | var _param2 = _interopRequireDefault(_jqueryParam); 25 | 26 | var g = undefined; 27 | if (typeof self !== 'undefined') { 28 | g = self; 29 | } 30 | if (typeof global !== 'undefined') { 31 | g = global; 32 | } 33 | if (typeof window !== 'undefined') { 34 | g = window; 35 | } 36 | 37 | // https://github.com/jquery/jquery/blob/master/src/ajax.js#L20 38 | var rnoContent = /^(?:GET|HEAD)$/; 39 | 40 | // https://github.com/github/fetch/blob/master/fetch.js#L113 41 | var support = { 42 | blob: 'FileReader' in g && 'Blob' in g && (function () { 43 | try { 44 | new Blob(); 45 | return true; 46 | } catch (e) { 47 | return false; 48 | } 49 | })(), 50 | formData: 'FormData' in g 51 | }; 52 | 53 | var parseXML = function parseXML(text, mimeType) { 54 | if (g) { 55 | // in browser 56 | // https://github.com/jquery/jquery/blob/master/src/ajax/parseXML.js 57 | try { 58 | xml = new g.DOMParser().parseFromString(text, mime); 59 | } catch (e) { 60 | xml = undefined; 61 | } 62 | if (!xml || xml.getElementsByTagName('parsererror').length) { 63 | throw new Error('Invalid XML: ' + text); 64 | } 65 | } else { 66 | // node, return plain text 67 | xml = text; 68 | } 69 | return xml; 70 | }; 71 | 72 | var resXML = function resXML(res, mimeType) { 73 | var xml; 74 | var type = mimeType; 75 | var mime = type ? type.split(';').unshift() : 'text/xml'; 76 | var text = res.text(); 77 | return text.then(function (text) { 78 | return parseXML(text, mime); 79 | }); 80 | }; 81 | 82 | var resText = function resText(res) { 83 | return res.text(); 84 | }; 85 | 86 | var resTractors = { 87 | arraybuffer: function arraybuffer(res) { 88 | return res.arrayBuffer(); 89 | }, 90 | blob: function blob(res) { 91 | return res.blob(); 92 | }, 93 | formdata: function formdata(res) { 94 | return res.formData(); 95 | }, 96 | html: resText, 97 | json: function json(res) { 98 | return res.json(); 99 | }, 100 | plain: resText, 101 | text: resText, 102 | xml: resXML 103 | }; 104 | 105 | var isCORS = function isCORS(url) { 106 | if (g.document && g.document.location && /^\w+:\/\//.test(url)) { 107 | var frags = url.replace(/^\w+:\/\//, ''); 108 | var index = url.indexOf('/'); 109 | var hostname = frags.substr(0, index); 110 | return hostname !== g.document.location.hostname; 111 | } 112 | return false; 113 | }; 114 | 115 | var shortContentType = { 116 | '*': '*/*', 117 | json: 'application/json', 118 | text: 'text/plain', 119 | xml: 'application/xml' 120 | }; 121 | 122 | var normalizeContentType = function normalizeContentType(contentType) { 123 | return shortContentType[contentType] || contentType; 124 | }; 125 | 126 | var Fetcher = (function () { 127 | function Fetcher() { 128 | _classCallCheck(this, Fetcher); 129 | 130 | this.options = { 131 | method: 'get', 132 | converters: { 133 | 'text json': JSON.parse, 134 | 'text xml': parseXML 135 | } 136 | }; 137 | } 138 | 139 | _createClass(Fetcher, [{ 140 | key: 'param', 141 | value: function param(data) { 142 | return (0, _param2['default'])(data); 143 | } 144 | }, { 145 | key: 'setup', 146 | value: function setup(options) { 147 | for (var k in options) { 148 | var v = options[k]; 149 | if (typeof v === 'object') { 150 | for (var kk in options) { 151 | var vv = options[kk]; 152 | this.options[k][kk] = vv; 153 | } 154 | } else { 155 | this.options[k] = v; 156 | } 157 | } 158 | } 159 | }, { 160 | key: 'request', 161 | value: function request(method, url, data) { 162 | var _this = this; 163 | 164 | var options = arguments[3] === undefined ? {} : arguments[3]; 165 | 166 | var m = method || options.method || options.type || this.options.method || 'get'; 167 | options.method = m.trim().toUpperCase(); 168 | 169 | if (options.headers && options.headers['Content-Type']) { 170 | options.headers['Content-Type'] = normalizeContentType(options.headers['Content-Type']); 171 | } 172 | if (options.contentType) { 173 | options.contentType = normalizeContentType(options.contentType); 174 | } 175 | var headers = new Headers(options.headers || {}); 176 | options.headers = headers; 177 | 178 | // auto set to cors if hotname is different 179 | if (!options.mode && isCORS(url)) { 180 | options.mode = 'cors'; 181 | } 182 | 183 | if (rnoContent.test(options.method)) { 184 | // set query parameter got GET/HEAD 185 | var query = this.param(data); 186 | if (query) { 187 | url = url + (/\?/.test(url) ? '&' : '?') + query; 188 | } 189 | } else { 190 | // Other method will have request body 191 | 192 | // grab and delete Content-Type header 193 | // fetch will set Content-Type for common cases 194 | var contentType = options.contentType || this.options.contentType || headers.get('Content-Type'); 195 | headers['delete']('Content-Type'); 196 | 197 | // set body 198 | if (typeof data === 'string' || support.formdata && FormData.prototype.isPrototypeOf(data) || support.blob && Blob.prototype.isPrototypeOf(data)) { 199 | if (contentType) { 200 | headers.set('Content-Type', contentType); 201 | } 202 | options.body = data; 203 | } else if (contentType === 'application/json') { 204 | headers.set('Content-Type', contentType); 205 | options.body = JSON.stringify(data); 206 | } else if (data) { 207 | // x-www-form-urlencoded is default in fetch 208 | options.body = this.param(data); 209 | } 210 | } 211 | 212 | var extractor; 213 | 214 | var dataType = options.dataType || this.options.dataType || '*'; 215 | dataType = dataType.trim(); 216 | dataType = dataType === '*' ? '' : dataType; 217 | 218 | var mimeType = options.mimeType || this.options.mimeType || ''; 219 | mimeType = mimeType.trim(); 220 | 221 | var accepts = options.accepts || '*/*'; 222 | 223 | if (dataType && shortContentType[dataType]) { 224 | accepts = shortContentType[dataType]; 225 | if (dataType !== '*') { 226 | accepts += ', ' + shortContentType['*'] + '; q=0.01'; 227 | } 228 | } 229 | 230 | delete options.dataType; 231 | delete options.mimeType; 232 | delete options.accepts; 233 | 234 | headers.set('Accept', accepts); 235 | 236 | var racers = []; 237 | 238 | var timeout = options.timeout || this.options.timeout; 239 | if (typeof timeout === 'number') { 240 | racers.push(new Promise(function (resolve, reject) { 241 | setTimeout(function () { 242 | reject([new Error('timeout')]); 243 | }, timeout); 244 | })); 245 | delete options.timeout; 246 | } 247 | 248 | racers.push(fetch(url, options).then(function (res) { 249 | var statusText = res.statusText; 250 | if (!res.ok && res.status !== 304) { 251 | return Promise.reject([new Error(statusText), res]); 252 | } 253 | 254 | if (res.status === 204 || options.method === 'HEAD') { 255 | // if no content 256 | statusText = 'nocontent'; 257 | } else if (res.status === 304) { 258 | // if not modified 259 | statusText = 'notmodified'; 260 | } else { 261 | statusText = 'success'; 262 | } 263 | 264 | var contentType = res.headers.get('Content-Type') || ''; 265 | var second = function second(value) { 266 | return value; 267 | }; 268 | 269 | mimeType = mimeType || contentType.split(';').shift(); 270 | dataType = mimeType.split(/[\/+]/).pop().toLowerCase() || dataType || 'text'; 271 | extractor = resTractors[dataType]; 272 | 273 | if (!extractor && typeof options.converters === 'object') { 274 | for (var fromto in options.converters) { 275 | var _fromto$split = fromto.split(' '); 276 | 277 | var _fromto$split2 = _slicedToArray(_fromto$split, 2); 278 | 279 | var from = _fromto$split2[0]; 280 | var to = _fromto$split2[1]; 281 | 282 | if (to === dataType && resTractors[from]) { 283 | extractor = resTractors[from]; 284 | second = _this.options.converters[fromto]; 285 | break; 286 | } 287 | } 288 | } 289 | 290 | if (!extractor) { 291 | for (var fromto in _this.options.converters) { 292 | if (options.converters[fromto]) { 293 | continue; 294 | } 295 | 296 | var _fromto$split3 = fromto.split(' '); 297 | 298 | var _fromto$split32 = _slicedToArray(_fromto$split3, 2); 299 | 300 | var from = _fromto$split32[0]; 301 | var to = _fromto$split32[1]; 302 | 303 | if (to === dataType && resTractors[from]) { 304 | extractor = resTractors[from]; 305 | second = _this.options.converters[fromto]; 306 | break; 307 | } 308 | } 309 | } 310 | 311 | var value = extractor ? extractor(res, mimeType).then(second) : Promise.reject(new Error('No converter for response to ' + dataType)); 312 | 313 | return Promise.all([value, statusText, res])['catch'](function (error) { 314 | throw [error, res]; 315 | }); 316 | }, function (error) { 317 | throw [error]; 318 | })); 319 | 320 | return Promise.race(racers); 321 | } 322 | }, { 323 | key: 'delete', 324 | value: function _delete(url, data, options) { 325 | return this.request('DELETE', url, data, options); 326 | } 327 | }, { 328 | key: 'get', 329 | value: function get(url, data, options) { 330 | return this.request('GET', url, data, options); 331 | } 332 | }, { 333 | key: 'getJSON', 334 | value: function getJSON(url, data) { 335 | var options = arguments[2] === undefined ? {} : arguments[2]; 336 | 337 | options.dataType = 'json'; 338 | return this.get(url, data, options); 339 | } 340 | }, { 341 | key: 'head', 342 | value: function head(url, data, options) { 343 | return this.request('HEAD', url, data, options); 344 | } 345 | }, { 346 | key: 'options', 347 | value: function options(url, data, _options) { 348 | return this.request('OPTIONS', url, data, _options); 349 | } 350 | }, { 351 | key: 'post', 352 | value: function post(url, data, options) { 353 | return this.request('POST', url, data, options); 354 | } 355 | }, { 356 | key: 'put', 357 | value: function put(url, data, options) { 358 | return this.request('PUT', url, data, options); 359 | } 360 | }]); 361 | 362 | return Fetcher; 363 | })(); 364 | 365 | ; 366 | 367 | module.exports = new Fetcher(); 368 | }); 369 | 370 | -------------------------------------------------------------------------------- /fetcher.node.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define('fetcher', ['exports', 'module', 'jquery-param'], factory); 4 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { 5 | factory(exports, module, require('jquery-param')); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, mod, global.param); 11 | global.fetcher = mod.exports; 12 | } 13 | })(this, function (exports, module, _jqueryParam) { 14 | 'use strict'; 15 | if (global && !global.Headers) { 16 | global.Headers = require('node-fetch/lib/headers'); 17 | global.fetch = require('node-fetch'); 18 | } 19 | 20 | var _interopRequire = function (obj) { return obj && obj.__esModule ? obj['default'] : obj; }; 21 | 22 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; 23 | 24 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 25 | 26 | var _param2 = _interopRequire(_jqueryParam); 27 | 28 | var self = undefined; 29 | if (typeof global !== 'undefined') { 30 | self = global; 31 | } 32 | if (typeof window !== 'undefined') { 33 | self = window; 34 | } 35 | 36 | // https://github.com/jquery/jquery/blob/master/src/ajax.js#L20 37 | var rnoContent = /^(?:GET|HEAD)$/; 38 | 39 | // https://github.com/github/fetch/blob/master/fetch.js#L113 40 | var support = { 41 | blob: 'FileReader' in self && 'Blob' in self && (function () { 42 | try { 43 | new Blob(); 44 | return true; 45 | } catch (e) { 46 | return false; 47 | } 48 | })(), 49 | formData: 'FormData' in self 50 | }; 51 | 52 | var parseXML = function parseXML(res, mimeType) { 53 | var xml; 54 | var type = mimeType; 55 | var mime = type ? type.split(';').unshift() : 'text/xml'; 56 | var text = res.text(); 57 | if (self) { 58 | // in browser 59 | // https://github.com/jquery/jquery/blob/master/src/ajax/parseXML.js 60 | try { 61 | xml = new self.DOMParser().parseFromString(text, mime); 62 | } catch (e) { 63 | xml = undefined; 64 | } 65 | if (!xml || xml.getElementsByTagName('parsererror').length) { 66 | throw new Error('Invalid XML: ' + text); 67 | } 68 | } else { 69 | // node, return plain text 70 | xml = text; 71 | } 72 | return Promise.resolve(xml); 73 | }; 74 | 75 | var resText = function resText(res) { 76 | return res.text(); 77 | }; 78 | 79 | var resTractors = { 80 | arraybuffer: function arraybuffer(res) { 81 | return res.arrayBuffer(); 82 | }, 83 | blob: function blob(res) { 84 | return res.blob(); 85 | }, 86 | formdata: function formdata(res) { 87 | return res.formData(); 88 | }, 89 | html: resText, 90 | json: function json(res) { 91 | return res.json(); 92 | }, 93 | plain: resText, 94 | text: resText, 95 | xml: parseXML 96 | }; 97 | 98 | var isCORS = function isCORS(url) { 99 | if (self.document && self.document.location && /^\w+:\/\//.test(url)) { 100 | var frags = url.replace(/^\w+:\/\//, ''); 101 | var index = url.indexOf('/'); 102 | var hostname = frags.substr(0, index); 103 | return hostname !== self.document.location.hostname; 104 | } 105 | return false; 106 | }; 107 | 108 | var shortContentType = { 109 | '*': '*/*', 110 | json: 'application/json', 111 | text: 'text/plain', 112 | xml: 'application/xml' 113 | }; 114 | 115 | var normalizeContentType = function normalizeContentType(contentType) { 116 | return shortContentType[contentType] || contentType; 117 | }; 118 | 119 | var Fetcher = (function () { 120 | function Fetcher() { 121 | _classCallCheck(this, Fetcher); 122 | } 123 | 124 | _createClass(Fetcher, [{ 125 | key: 'param', 126 | value: (function (_param) { 127 | function param(_x) { 128 | return _param.apply(this, arguments); 129 | } 130 | 131 | param.toString = function () { 132 | return _param.toString(); 133 | }; 134 | 135 | return param; 136 | })(function (data) { 137 | return _param2(data); 138 | }) 139 | }, { 140 | key: 'request', 141 | value: function request(method, url, data) { 142 | var options = arguments[3] === undefined ? {} : arguments[3]; 143 | 144 | options.method = method.toUpperCase(); 145 | 146 | if (options.headers && options.headers['Content-Type']) { 147 | options.headers['Content-Type'] = normalizeContentType(options.headers['Content-Type']); 148 | } 149 | var headers = new Headers(options.headers || {}); 150 | options.headers = headers; 151 | 152 | // auto set to cors if hotname is different 153 | if (!options.mode && isCORS(url)) { 154 | options.mode = 'cors'; 155 | } 156 | 157 | if (rnoContent.test(options.method)) { 158 | // set query parameter got GET/HEAD 159 | var query = this.param(data); 160 | if (query) { 161 | url = url + (/\?/.test(url) ? '&' : '?') + query; 162 | } 163 | } else { 164 | // Other method will have request body 165 | 166 | // grab and delete Content-Type header 167 | // fetch will set Content-Type for common cases 168 | var contentType = options.contentType || headers.get('Content-Type'); 169 | headers['delete']('Content-Type'); 170 | 171 | // set body 172 | if (typeof data === 'string' || support.formdata && FormData.prototype.isPrototypeOf(data) || support.blob && Blob.prototype.isPrototypeOf(data)) { 173 | if (contentType) { 174 | headers.set('Content-Type', contentType); 175 | } 176 | options.body = data; 177 | } else if (contentType === 'application/json') { 178 | headers.set('Content-Type', contentType); 179 | options.body = JSON.stringify(data); 180 | } else if (data) { 181 | // x-www-form-urlencoded is default in fetch 182 | options.body = this.param(data); 183 | } 184 | } 185 | 186 | var extractor = null; 187 | var dataType = options.dataType ? options.dataType.trim() : '*'; 188 | var accept = '*/*'; 189 | if (dataType && shortContentType[dataType]) { 190 | accept = shortContentType[dataType]; 191 | if (dataType !== '*') { 192 | accept += ', ' + shortContentType['*'] + '; q=0.01'; 193 | extractor = resTractors[dataType.toLowerCase()]; 194 | } 195 | } 196 | 197 | if (options.mimeType) { 198 | var mimeType = options.mimeType.trim(); 199 | } 200 | 201 | delete options.dataType; 202 | delete options.mimeType; 203 | 204 | headers.set('Accept', accept); 205 | 206 | var racers = []; 207 | if (options.timeout) { 208 | if (typeof options.timeout === 'number') { 209 | racers.push(new Promise(function (resolve, reject) { 210 | setTimeout(function () { 211 | reject([new Error('timeout')]); 212 | }, options.timeout); 213 | })); 214 | } 215 | delete options.timeout; 216 | } 217 | 218 | racers.push(fetch(url, options).then(function (res) { 219 | var statusText = res.statusText; 220 | if (!res.ok && res.status !== 304) { 221 | return Promise.reject([statusText, res]); 222 | } 223 | 224 | if (res.status === 204 || options.method === 'HEAD') { 225 | // if no content 226 | statusText = 'nocontent'; 227 | } else if (res.status === 304) { 228 | // if not modified 229 | statusText = 'notmodified'; 230 | } else { 231 | statusText = 'success'; 232 | } 233 | 234 | var contentType = res.headers.get('Content-Type') || ''; 235 | mimeType = mimeType || contentType.split(';').shift(); 236 | if (!extractor) { 237 | dataType = mimeType.split(/[\/+]/).pop(); 238 | extractor = resTractors[dataType.toLowerCase()] || resTractors.text; 239 | } 240 | return Promise.all([extractor(res, mimeType), statusText, res]); 241 | }, function (error) { 242 | throw [error]; 243 | })); 244 | 245 | return Promise.race(racers); 246 | } 247 | }, { 248 | key: 'delete', 249 | value: function _delete(url, data, options) { 250 | return this.request('DELETE', url, data, options); 251 | } 252 | }, { 253 | key: 'get', 254 | value: function get(url, data, options) { 255 | return this.request('GET', url, data, options); 256 | } 257 | }, { 258 | key: 'getJSON', 259 | value: function getJSON(url, data) { 260 | var options = arguments[2] === undefined ? {} : arguments[2]; 261 | 262 | options.dataType = 'json'; 263 | return this.get(url, data, options); 264 | } 265 | }, { 266 | key: 'head', 267 | value: function head(url, data, options) { 268 | return this.request('HEAD', url, data, options); 269 | } 270 | }, { 271 | key: 'options', 272 | value: (function (_options) { 273 | function options(_x2, _x3, _x4) { 274 | return _options.apply(this, arguments); 275 | } 276 | 277 | options.toString = function () { 278 | return _options.toString(); 279 | }; 280 | 281 | return options; 282 | })(function (url, data, options) { 283 | return this.request('OPTIONS', url, data, options); 284 | }) 285 | }, { 286 | key: 'post', 287 | value: function post(url, data, options) { 288 | return this.request('POST', url, data, options); 289 | } 290 | }, { 291 | key: 'put', 292 | value: function put(url, data, options) { 293 | return this.request('PUT', url, data, options); 294 | } 295 | }]); 296 | 297 | return Fetcher; 298 | })(); 299 | 300 | ; 301 | 302 | module.exports = new Fetcher(); 303 | }); 304 | 305 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | var aster = require("aster"); 2 | var concat = require("aster-concat"); 3 | 4 | 5 | aster.src.registerParser('.js', require('aster-parse-babel')); 6 | 7 | aster.src(["fetcher.es6.js"]) 8 | .map(concat('fetcher.js')) 9 | .map(aster.dest('./', {comment: true})) 10 | .subscribe(aster.runner); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-er", 3 | "version": "0.0.10", 4 | "description": "WHATWG fetch helper", 5 | "homepage": "https://github.com/othree/fetcher", 6 | "author": { 7 | "name": "othree", 8 | "url": "https://github.com/othree" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/othree/fetcher.git" 13 | }, 14 | "browser": "dist/fetcher.js", 15 | "main": "fetcher.node.js", 16 | "engines": { 17 | "node": ">=0.8.0" 18 | }, 19 | "dependencies": { 20 | "node-fetch": "*", 21 | "jquery-param": "*" 22 | }, 23 | "devDependencies": { 24 | "es6-promise": "*", 25 | "expect": "*", 26 | "istanbul": "*", 27 | "mocha": "*", 28 | "should": "*", 29 | "should-promised": "^0.2.1", 30 | "sinon": "*" 31 | }, 32 | "scripts": { 33 | "test": "mocha" 34 | }, 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /param.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT 3 | */ 4 | /*jslint forin: true, plusplus: true */ 5 | /*global module, define */ 6 | (function (global) { 7 | 'use strict'; 8 | 9 | var param = function (a) { 10 | var add = function (s, k, v) { 11 | v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; 12 | s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v); 13 | }, buildParams = function (prefix, obj, s) { 14 | var i, len, key; 15 | 16 | if (Object.prototype.toString.call(obj) === '[object Array]') { 17 | for (i = 0, len = obj.length; i < len; i++) { 18 | buildParams(prefix + '[' + (typeof obj[i] === 'object' ? i : '') + ']', obj[i], s); 19 | } 20 | } else if (obj && obj.toString() === '[object Object]') { 21 | for (key in obj) { 22 | if (obj.hasOwnProperty(key)) { 23 | if (prefix) { 24 | buildParams(prefix + '[' + key + ']', obj[key], s, add); 25 | } else { 26 | buildParams(key, obj[key], s, add); 27 | } 28 | } 29 | } 30 | } else if (prefix) { 31 | add(s, prefix, obj); 32 | } else { 33 | for (key in obj) { 34 | add(s, key, obj[key]); 35 | } 36 | } 37 | return s; 38 | }; 39 | return buildParams('', a, []).join('&').replace(/%20/g, '+'); 40 | }; 41 | 42 | if (typeof module === 'object' && typeof module.exports === 'object') { 43 | module.exports = param; 44 | } else if (typeof define === 'function' && define.amd) { 45 | define([], function () { 46 | return param; 47 | }); 48 | } else { 49 | global.param = param; 50 | } 51 | 52 | }(this)); 53 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require('mocha'); 2 | require('should-promised'); 3 | 4 | var stream = require('stream'); 5 | var expect = require('expect'); 6 | var sinon = require('sinon'); 7 | 8 | global.Promise = require('es6-promise').Promise; 9 | global.fetcher = require('../'); 10 | 11 | global.Headers = require('node-fetch/lib/headers'); 12 | global.Response = require('node-fetch/lib/response'); 13 | 14 | 15 | function once(fn) { 16 | var returnValue, called = false; 17 | return function () { 18 | if (!called) { 19 | called = true; 20 | returnValue = fn.apply(this, arguments); 21 | } 22 | return returnValue; 23 | }; 24 | } 25 | 26 | describe('Domain Model', function() { 27 | 28 | it("GET request parameter", function () { 29 | var callback = sinon.stub().returns(new Promise(function (resolver) { 30 | resolve('{"bcd": 234}'); 31 | })); 32 | global.fetch = once(callback); 33 | 34 | fetcher.get('/', {abc: 123}); 35 | 36 | callback.called.should.be.true; 37 | 38 | var args = callback.getCall(0).args; 39 | var url = args[0]; 40 | var opt = args[1]; 41 | 42 | url.should.equal('/?abc=123'); 43 | opt.method.should.equal('GET') 44 | }); 45 | 46 | it("POST request parameter, use 'application/json'", function () { 47 | var callback = sinon.stub().returns(new Promise(function (resolver) { 48 | resolve('{"bcd": 234}'); 49 | })); 50 | global.fetch = once(callback); 51 | 52 | fetcher.post('/', {abc: 123}, {headers: {"Content-Type": "application/json"}}); 53 | 54 | callback.called.should.be.true; 55 | 56 | var args = callback.getCall(0).args; 57 | var url = args[0]; 58 | var opt = args[1]; 59 | url.should.equal('/'); 60 | 61 | var body = args[1].body; 62 | body.should.equal('{"abc":123}'); 63 | opt.method.should.equal('POST') 64 | }); 65 | 66 | it("POST request parameter, use 'json'", function () { 67 | var callback = sinon.stub().returns(new Promise(function (resolver) { 68 | resolve('{"bcd": 234}'); 69 | })); 70 | global.fetch = once(callback); 71 | 72 | fetcher.post('/', {abc: 123}, {headers: {"Content-Type": "json"}}); 73 | 74 | callback.called.should.be.true; 75 | 76 | var args = callback.getCall(0).args; 77 | var url = args[0]; 78 | url.should.equal('/'); 79 | 80 | var body = args[1].body; 81 | body.should.equal('{"abc":123}'); 82 | }); 83 | 84 | it("POST request parameter, use json string", function () { 85 | var callback = sinon.stub().returns(new Promise(function (resolver) { 86 | resolve('{"bcd": 234}'); 87 | })); 88 | global.fetch = once(callback); 89 | 90 | fetcher.post('/', JSON.stringify({abc: 123}), {headers: {"Content-Type": "application/json"}}); 91 | 92 | callback.called.should.be.true; 93 | 94 | var args = callback.getCall(0).args; 95 | var url = args[0]; 96 | url.should.equal('/'); 97 | 98 | var body = args[1].body; 99 | body.should.equal('{"abc":123}'); 100 | }); 101 | 102 | it("POST request without Content-Type", function () { 103 | var callback = sinon.stub().returns(new Promise(function (resolver) { 104 | resolve('{"bcd": 234}'); 105 | })); 106 | global.fetch = once(callback); 107 | 108 | fetcher.post('/', {abc: 123, def: 456}); 109 | 110 | callback.called.should.be.true; 111 | 112 | var args = callback.getCall(0).args; 113 | var url = args[0]; 114 | url.should.equal('/'); 115 | 116 | var body = args[1].body; 117 | body.should.equal('abc=123&def=456'); 118 | }); 119 | 120 | it("PUT request without Content-Type", function () { 121 | var callback = sinon.stub().returns(new Promise(function (resolver) { 122 | resolve('{"bcd": 234}'); 123 | })); 124 | global.fetch = once(callback); 125 | 126 | fetcher.put('/', {abc: 123, def: 456}); 127 | 128 | callback.called.should.be.true; 129 | 130 | var args = callback.getCall(0).args; 131 | var url = args[0]; 132 | var opt = args[1]; 133 | url.should.equal('/'); 134 | 135 | var body = args[1].body; 136 | body.should.equal('abc=123&def=456'); 137 | opt.method.should.equal('PUT') 138 | }); 139 | 140 | it("Response JSON with dataType option", function (done) { 141 | var data = {bcd: 456}; 142 | var body = new stream.PassThrough(); 143 | body.end(JSON.stringify(data)); 144 | var res = new Response( 145 | body, 146 | { 147 | url: '/', 148 | status: 200, 149 | headers: (new Headers()), 150 | size: 12, 151 | timeout: 5000 152 | } 153 | ); 154 | var callback = sinon.stub().returns(Promise.resolve(res)); 155 | global.fetch = once(callback); 156 | 157 | var r = fetcher.post('/', {abc: 123, def: 456}, {dataType: 'json'}); 158 | 159 | 160 | callback.called.should.be.true; 161 | 162 | r.then(function (v) { 163 | v[0]['bcd'].should.equal(456); 164 | done(); 165 | }); 166 | }); 167 | 168 | it("Response JSON with Content-Type", function (done) { 169 | var data = {bcd: 456}; 170 | var body = new stream.PassThrough(); 171 | var res = new Response( 172 | body, 173 | { 174 | url: '/', 175 | status: 200, 176 | headers: (new Headers({ 177 | "Content-Type": 'application/json' 178 | })), 179 | size: 12, 180 | timeout: 5000 181 | } 182 | ); 183 | var callback = sinon.stub().returns(new Promise(function (resolve) {resolve(res)})); 184 | global.fetch = once(callback); 185 | 186 | var r = fetcher.post('/', {abc: 123, def: 456}); 187 | 188 | body.end(JSON.stringify(data)); 189 | 190 | callback.called.should.be.true; 191 | 192 | r.then(function (v) { 193 | v[0]['bcd'].should.equal(456); 194 | done(); 195 | }); 196 | }); 197 | 198 | it("Response text without Content-Type", function (done) { 199 | var data = {bcd: 456}; 200 | var body = new stream.PassThrough(); 201 | var res = new Response( 202 | body, 203 | { 204 | url: '/', 205 | status: 200, 206 | headers: (new Headers({})), 207 | size: 12, 208 | timeout: 5000 209 | } 210 | ); 211 | var callback = sinon.stub().returns(Promise.resolve(res)); 212 | global.fetch = once(callback); 213 | 214 | var r = fetcher.post('/', {abc: 123, def: 456}); 215 | 216 | body.end(JSON.stringify(data)); 217 | 218 | callback.called.should.be.true; 219 | 220 | r.then(function (v) { 221 | v[0].should.equal(JSON.stringify(data)); 222 | done(); 223 | }); 224 | }); 225 | 226 | it("Timeout", function (done) { 227 | var data = {bcd: 456}; 228 | var callback = sinon.stub().returns((new Promise(function () {}))); 229 | global.fetch = once(callback); 230 | 231 | var r = fetcher.post('/timeout', {abc: 123, def: 456}, {timeout: 1000}); 232 | 233 | callback.called.should.be.true; 234 | 235 | r.then(null, function (err) { 236 | err[0].toString().should.equal('Error: timeout'); 237 | done(); 238 | }); 239 | }); 240 | 241 | }); 242 | 243 | /* 244 | * Cases 245 | * response text 246 | */ 247 | --------------------------------------------------------------------------------