_.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. var utils = require('./utils/utils');
  2. var isBrowser = typeof document !== "undefined";
  3. class Fly {
  4. constructor(engine) {
  5. this.engine = engine || XMLHttpRequest;
  6. this.default = this //For typeScript
  7. /**
  8. * Add lock/unlock API for interceptor.
  9. *
  10. * Once an request/response interceptor is locked, the incoming request/response
  11. * will be added to a queue before they enter the interceptor, they will not be
  12. * continued until the interceptor is unlocked.
  13. *
  14. * @param [interceptor] either is interceptors.request or interceptors.response
  15. */
  16. function wrap(interceptor) {
  17. var resolve;
  18. var reject;
  19. function _clear() {
  20. interceptor.p = resolve = reject = null;
  21. }
  22. utils.merge(interceptor, {
  23. lock() {
  24. if (!resolve) {
  25. interceptor.p = new Promise((_resolve, _reject) => {
  26. resolve = _resolve
  27. reject = _reject;
  28. })
  29. }
  30. },
  31. unlock() {
  32. if (resolve) {
  33. resolve()
  34. _clear();
  35. }
  36. },
  37. clear() {
  38. if (reject) {
  39. reject("cancel");
  40. _clear();
  41. }
  42. }
  43. })
  44. }
  45. var interceptors = this.interceptors = {
  46. response: {
  47. use(handler, onerror) {
  48. this.handler = handler;
  49. this.onerror = onerror;
  50. }
  51. },
  52. request: {
  53. use(handler) {
  54. this.handler = handler;
  55. }
  56. }
  57. }
  58. var irq = interceptors.request;
  59. var irp = interceptors.response;
  60. wrap(irp);
  61. wrap(irq);
  62. this.config = {
  63. method: "GET",
  64. baseURL: "",
  65. headers: {},
  66. timeout: 0,
  67. parseJson: true, // Convert response data to JSON object automatically.
  68. withCredentials: false
  69. }
  70. }
  71. request(url, data, options) {
  72. var engine = new this.engine;
  73. var contentType = "Content-Type";
  74. var interceptors = this.interceptors;
  75. var requestInterceptor = interceptors.request;
  76. var responseInterceptor = interceptors.response;
  77. var requestInterceptorHandler = requestInterceptor.handler;
  78. var promise = new Promise((resolve, reject) => {
  79. if (utils.isObject(url)) {
  80. options = url;
  81. url = options.url;
  82. }
  83. options = options || {};
  84. options.headers = options.headers || {};
  85. function isPromise(p) {
  86. // some polyfill implementation of Promise may be not standard,
  87. // so, we test by duck-typing
  88. return p && p.then && p.catch
  89. }
  90. /**
  91. * If the request/response interceptor has been locked,
  92. * the new request/response will enter a queue. otherwise, it will be performed directly.
  93. * @param [promise] if the promise exist, means the interceptor is locked.
  94. * @param [callback]
  95. */
  96. function enqueueIfLocked(promise, callback) {
  97. if (promise) {
  98. promise.then(() => {
  99. callback()
  100. })
  101. } else {
  102. callback()
  103. }
  104. }
  105. // make the http request
  106. function makeRequest(options) {
  107. data = options.body;
  108. // Normalize the request url
  109. url = utils.trim(options.url);
  110. var baseUrl = utils.trim(options.baseURL || "");
  111. if (!url && isBrowser && !baseUrl) url = location.href;
  112. if (url.indexOf("http") !== 0) {
  113. var isAbsolute = url[0] === "/";
  114. if (!baseUrl && isBrowser) {
  115. var arr = location.pathname.split("/");
  116. arr.pop();
  117. baseUrl = location.protocol + "//" + location.host + (isAbsolute ? "" : arr.join("/"))
  118. }
  119. if (baseUrl[baseUrl.length - 1] !== "/") {
  120. baseUrl += "/"
  121. }
  122. url = baseUrl + (isAbsolute ? url.substr(1) : url)
  123. if (isBrowser) {
  124. // Normalize the url which contains the ".." or ".", such as
  125. // "http://xx.com/aa/bb/../../xx" to "http://xx.com/xx" .
  126. var t = document.createElement("a");
  127. t.href = url;
  128. url = t.href;
  129. }
  130. }
  131. var responseType = utils.trim(options.responseType || "")
  132. engine.withCredentials = !!options.withCredentials;
  133. var isGet = options.method === "GET";
  134. if (isGet) {
  135. if (data) {
  136. if (utils.type(data) !== "string") {
  137. data = utils.formatParams(data);
  138. }
  139. url += (url.indexOf("?") === -1 ? "?" : "&") + data;
  140. }
  141. }
  142. engine.open(options.method, url);
  143. // try catch for ie >=9
  144. try {
  145. engine.timeout = options.timeout || 0;
  146. if (responseType !== "stream") {
  147. engine.responseType = responseType
  148. }
  149. } catch (e) {
  150. }
  151. var customContentType = options.headers[contentType] || options.headers[contentType.toLowerCase()];
  152. // default content type
  153. var _contentType = "application/x-www-form-urlencoded";
  154. // If the request data is json object, transforming it to json string,
  155. // and set request content-type to "json". In browser, the data will
  156. // be sent as RequestBody instead of FormData
  157. if (utils.trim((customContentType || "").toLowerCase()) === _contentType) {
  158. data = utils.formatParams(data);
  159. } else if (!utils.isFormData(data) && ["object", "array"].indexOf(utils.type(data)) !== -1) {
  160. _contentType = 'application/json;charset=utf-8'
  161. data = JSON.stringify(data);
  162. }
  163. //If user doesn't set content-type, set default.
  164. if (!customContentType) {
  165. options.headers[contentType] = _contentType;
  166. }
  167. for (var k in options.headers) {
  168. if (k === contentType && utils.isFormData(data)) {
  169. // Delete the content-type, Let the browser set it
  170. delete options.headers[k];
  171. } else {
  172. try {
  173. // In browser environment, some header fields are readonly,
  174. // write will cause the exception .
  175. engine.setRequestHeader(k, options.headers[k])
  176. } catch (e) {
  177. }
  178. }
  179. }
  180. function onresult(handler, data, type) {
  181. enqueueIfLocked(responseInterceptor.p, function () {
  182. if (handler) {
  183. //如果失败,添加请求信息
  184. if (type) {
  185. data.request = options;
  186. }
  187. var ret = handler.call(responseInterceptor, data, Promise)
  188. data = ret === undefined ? data : ret;
  189. }
  190. if (!isPromise(data)) {
  191. data = Promise[type === 0 ? "resolve" : "reject"](data)
  192. }
  193. data.then(d => {
  194. resolve(d)
  195. }).catch((e) => {
  196. reject(e)
  197. })
  198. })
  199. }
  200. function onerror(e) {
  201. e.engine = engine;
  202. onresult(responseInterceptor.onerror, e, -1)
  203. }
  204. function Err(msg, status) {
  205. this.message = msg
  206. this.status = status;
  207. }
  208. engine.onload = () => {
  209. // The xhr of IE9 has not response filed
  210. var response = engine.response || engine.responseText;
  211. if (response && options.parseJson && (engine.getResponseHeader(contentType) || "").indexOf("json") !== -1
  212. // Some third engine implementation may transform the response text to json object automatically,
  213. // so we should test the type of response before transforming it
  214. && !utils.isObject(response)) {
  215. response = JSON.parse(response);
  216. }
  217. var headers = {};
  218. var items = (engine.getAllResponseHeaders() || "").split("\r\n");
  219. items.pop();
  220. items.forEach((e) => {
  221. var key = e.split(":")[0]
  222. headers[key] = engine.getResponseHeader(key)
  223. })
  224. var status = engine.status
  225. var statusText = engine.statusText
  226. var data = {data: response, headers, status, statusText};
  227. // The _response filed of engine is set in adapter which be called in engine-wrapper.js
  228. utils.merge(data, engine._response)
  229. if ((status >= 200 && status < 300) || status === 304) {
  230. data.engine = engine;
  231. data.request = options;
  232. onresult(responseInterceptor.handler, data, 0)
  233. } else {
  234. var e = new Err(statusText, status);
  235. e.response = data
  236. onerror(e)
  237. }
  238. }
  239. engine.onerror = (e) => {
  240. onerror(new Err(e.msg || "Network Error", 0))
  241. }
  242. engine.ontimeout = () => {
  243. onerror(new Err(`timeout [ ${engine.timeout}ms ]`, 1))
  244. }
  245. engine._options = options;
  246. setTimeout(() => {
  247. engine.send(isGet ? null : data)
  248. }, 0)
  249. }
  250. enqueueIfLocked(requestInterceptor.p, () => {
  251. utils.merge(options, this.config)
  252. var headers = options.headers;
  253. headers[contentType] = headers[contentType] || headers[contentTypeLowerCase] || "";
  254. delete headers[contentTypeLowerCase]
  255. options.body = data || options.body;
  256. url = utils.trim(url || "");
  257. options.method = options.method.toUpperCase();
  258. options.url = url;
  259. var ret = options;
  260. if (requestInterceptorHandler) {
  261. ret = requestInterceptorHandler.call(requestInterceptor, options, Promise) || options;
  262. }
  263. if (!isPromise(ret)) {
  264. ret = Promise.resolve(ret)
  265. }
  266. ret.then((d) => {
  267. //if options continue
  268. if (d === options) {
  269. makeRequest(d)
  270. } else {
  271. resolve(d)
  272. }
  273. }, (err) => {
  274. reject(err)
  275. })
  276. })
  277. })
  278. promise.engine = engine;
  279. return promise;
  280. }
  281. all(promises) {
  282. return Promise.all(promises)
  283. }
  284. spread(callback) {
  285. return function (arr) {
  286. return callback.apply(null, arr);
  287. }
  288. }
  289. }
  290. //For typeScript
  291. Fly.default = Fly;
  292. ["get", "post", "put", "patch", "head", "delete"].forEach(e => {
  293. Fly.prototype[e] = function (url, data, option) {
  294. return this.request(url, data, utils.merge({method: e}, option))
  295. }
  296. })
  297. ["lock", "unlock", "clear"].forEach(e => {
  298. Fly.prototype[e] = function () {
  299. this.interceptors.request[e]();
  300. }
  301. })
  302. // Learn more about keep-loader: https://github.com/wendux/keep-loader
  303. KEEP("cdn||cdn-min", () => {
  304. // This code block will be removed besides the "CDN" and "cdn-min" build environment
  305. window.fly = new Fly;
  306. window.Fly = Fly;
  307. })
  308. module.exports = Fly;