UNPKG

jsdk-offical

Version:

JSDK is the most comprehensive TypeScript framework, like JDK.

487 lines (435 loc) 21.9 kB
/** * @project JSDK * @license MIT * @website https://github.com/fengboyue/jsdk * * @version 2.0.0 * @author Frank.Feng */ /// <reference path="../core/Promises.ts" /> /// <reference path="../util/Types.ts" /> /// <reference path="../util/Jsons.ts" /> /// <reference path="MIME.ts" /> module JS { export namespace net { export type HttpResponseType = 'xml' | 'html' | 'json' | 'text' | 'arraybuffer' | 'blob'; export interface HttpRequest { /** * Send in a new webwork thread. */ thread?: boolean | ThreadPreload; /** * An URL string for ajax request. */ url: string; /** * By default, all requests are sent asynchronously (i.e. this is set to true by default). */ async?: boolean; /** * If set to false, it will force requested pages not to be cached by the browser. Note: Setting cache * to false will only work correctly with HEAD and GET requests. It works by appending "_={timestamp}" * to the GET parameters. The parameter is not needed for other types of requests, except in IE8 when a * POST is made to a URL that has already been requested by a GET. */ cache?: boolean; /** * When sending data to the server, use this content type. Default is * "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases. If you explicitly * pass in a content-type to $.ajax(), then it is always sent to the server (even if no data is sent). * You can pass false to tell Http to not set any content type header. Note: The W3C * XMLHttpRequest specification dictates that the charset is always UTF-8; specifying another charset * will not force the browser to change the encoding. Note: For cross-domain requests, setting the * content type to anything other than application/x-www-form-urlencoded, multipart/form-data, or * text/plain will trigger the browser to send a preflight OPTIONS request to the server. */ requestMime?: string | false; /** * An object containing type converters. Each parser's value is a function that * returns the filtered response data of the type, will be replaced a default inner-parser. */ converts?: { html?: <T>(data: HTMLDocument) => T, xml?: <T>(data: XMLDocument) => T, text?: <T>(data: string) => T json?: <T>(data: JsonObject) => T arraybuffer?: <T>(data: ArrayBuffer) => T blob?: <T>(data: Blob) => T }; /** * Data to be sent to the server. */ data?: string | JsonObject | FormData | ArrayBuffer | Blob; /** * A function to be used to pre-handle the raw response data beforing parse the response. */ responseFilter?(raw: any, type: HttpResponseType): any; /** * "xml": Returns a XML document that can be processed.<br> * "html": Returns HTML as plain text; included script tags are evaluated when inserted in the DOM.<br> * "json": Evaluates the response as JSON and returns a JavaScript object.<br> * "text": A plain text string. */ responseType?: HttpResponseType; /** * An object of additional header key/value pairs to send along with requests using the XMLHttpRequest * transport. The header X-Requested-With: XMLHttpRequest is always added, but its default * XMLHttpRequest value can be changed here. */ headers?: JsonObject<string | null | undefined>; /** * Allow the request to be successful only if the response has changed since the last request. This is * done by checking the Last-Modified header. Default value is false, ignoring the header. This technique * also checks the 'etag' specified by the server to catch unmodified data. */ ifModified?: boolean; /** * The HTTP method to use for the request. */ method?: 'HEAD' | 'GET' | 'POST' | 'OPTIONS' | 'PUT' | 'DELETE'; /** * The XMLHttpRequest method overrideMimeType() specifies a MIME type other than the one provided by the server to be used instead when interpreting the data being transferred in a request. This may be used, for example, to force a stream to be treated and parsed as "text/xml", even if the server does not report it as such. */ overrideResponseMime?: string; /** * A username to be used with XMLHttpRequest in response to an HTTP access authentication request. */ username?: string; /** * A password to be used with XMLHttpRequest in response to an HTTP access authentication request. */ password?: string; /** * Set a timeout (in milliseconds) for the request. A value of 0 means there will be no timeout. */ timeout?: number; /** * True when credentials are to be included in a cross-origin request. False when they are to be excluded in a cross-origin request and when cookies are to be ignored in its response. Initially false. */ crossCookie?: boolean; /** * A function to be called when this request finishes (after success and error callbacks are executed). */ complete?: ((res: HttpResponse) => void); /** * A function to be called if this request succeeds. */ success?: ((res: HttpResponse) => void); /** * A function to be called if this request fails. */ error?: ((res: HttpResponse) => void); /** * A function to be called if this request progress. */ progress?: ((e: ProgressEvent, xhr: XMLHttpRequest) => void); } export interface HttpResponse { request: HttpRequest, type: HttpResponseType, raw: any, data: any, status: number, statusText: 'timeout' | 'abort' | 'parseerror' | 'nocontent' | 'notmodified' | string, headers: JsonObject<string>, xhr: XMLHttpRequest } export type HttpResponseConvert<INPUT, OUTPUT> = (data: INPUT, res: HttpResponse) => OUTPUT let Y = Types, J = Jsons, _judgeType = (t: string, dt: HttpResponseType): HttpResponseType => { if (MIME.text == t) return 'text'; if (MIME.html = t) return 'html'; if (MIME.xml == t) return 'xml'; if (MIME.json.indexOf(t) > -1) return 'json'; return dt; }, _headers = (xhr: XMLHttpRequest) => { let headers = {}, hString = xhr.getAllResponseHeaders(), hRegexp = /([^\s]*?):[ \t]*([^\r\n]*)/mg, match = null; while ((match = hRegexp.exec(hString))) { headers[match[1]] = match[2]; } return headers }, _response = (req: HttpRequest, xhr: XMLHttpRequest, error?: 'timeout' | 'abort' | 'error') => { let type = req.responseType, headers = _headers(xhr); //根据服务器返回的contentType自动推断type if (!type && xhr.status > 0) type = _judgeType(headers['Content-Type'], type); return <HttpResponse>{ request: req, url: xhr.responseURL, raw: xhr.response, type: type, data: xhr.response, status: xhr.status, statusText: error || (xhr.status == 0 ? 'error' : xhr.statusText), headers: headers, xhr: xhr } }, _parseResponse = function (this: PromiseContext<any>, res: HttpResponse, req: HttpRequest, xhr: XMLHttpRequest): any { try { let raw = req.responseType == 'xml' ? xhr.responseXML : xhr.response, cvt: Function = req.converts && req.converts[res.type]; if (req.responseFilter) raw = req.responseFilter(raw, res.type); res.data = cvt ? cvt(raw, res) : raw; } catch (e) { res.statusText = 'parseerror'; if (req.error) req.error(res); if (Http._ON['error']) Http._ON['error'](res); this.reject(res); } }, _error = function (this: PromiseContext<any>, req: HttpRequest, xhr: XMLHttpRequest, error: 'timeout' | 'abort' | 'error') { let res = _response(req, xhr, error); if (req.error) req.error(res); if (Http._ON['error']) Http._ON['error'](res); this.reject(res) }, CACHE = { lastModified: {}, etag: {} }, _done = function (this: PromiseContext<HttpResponse>, oURL: string, req: HttpRequest, xhr: XMLHttpRequest) { if (xhr['_isTimeout']) return;//已超时不处理 let status = xhr.status, isSucc = status >= 200 && status < 300 || status === 304, res: HttpResponse = _response(req, xhr); if (isSucc) { //cache modified let modified = null; if (req.ifModified) { modified = xhr.getResponseHeader('Last-Modified'); if (modified) CACHE.lastModified[oURL] = modified; modified = xhr.getResponseHeader('etag'); if (modified) CACHE.etag[oURL] = modified; } // if no content if (status === 204 || req.method === "HEAD") { res.statusText = 'nocontent' // if not modified } else if (status === 304) { res.statusText = 'notmodified' } //成功才解析返回数据 _parseResponse.call(this, res, req, xhr); } if (req.complete) req.complete(res); if (Http._ON['complete']) Http._ON['complete'](res); if (isSucc) { if (req.success) req.success(res); if (Http._ON['success']) Http._ON['success'](res); this.resolve(res) } else this.reject(res) }, _queryString = function (data: string | JsonObject | ArrayBuffer | Blob) { if (Y.isString(data)) { return encodeURI(<string>data) } else if (Y.isJsonObject(data)) { let str = ''; J.forEach(<JsonObject>data, (v, k) => { str += `&${k}=${encodeURIComponent(v)}`; }) return str; } return '' }, _queryURL = (req: HttpRequest) => { let url = req.url.replace(/^\/\//, location.protocol + '//'); if (!Check.isEmpty(req.data)) url = `${url}${url.indexOf('?') < 0 ? '?' : ''}${_queryString(req.data)}`; return url }, _finalURL = (url: string, cache: boolean) => { //uncached url url = url.replace(/([?&])_=[^&]*/, '$1'); if (!cache) url = `${url}${url.indexOf('?') < 0 ? '?' : '&'}_=${Date.now()}`; return url }, /** * 注意:从Gecko 30.0 (Firefox 30.0 / Thunderbird 30.0 / SeaMonkey 2.27),Blink 39.0和Edge 13开始, * 主线程上的同步请求由于对用户体验的负面影响而被弃用。 * 同步XHR通常会导致网络挂起。但开发人员通常不会注意到这个问题,因为在网络状况不佳或服务器响应速度慢的情况下, * 挂起只会显示同步XHR现在处于弃用状态。建议开发人员远离这个API。 * 同步XHR不允许所有新的XHR功能(如timeout或abort)。这样做会调用InvalidAccessError。 */ _send = function (this: PromiseContext<HttpResponse>, req: HttpRequest) { if (!req.url) JSLogger.error('Sent an ajax request without URL.') req = <HttpRequest>J.union(<HttpRequest>{ method: 'GET', crossCookie: false, async: true, responseType: 'text', cache: true }, req); let xhr: XMLHttpRequest = new XMLHttpRequest(), oURL = _queryURL(req), url = _finalURL(oURL, req.cache), reqType = req.requestMime, resType = req.responseType, headers = req.headers || {}; if (!reqType && (Y.isString(req.data) || Y.isJsonObject(req.data))) reqType = 'application/x-www-form-urlencoded;charset=UTF-8'; xhr.open(req.method, url, req.async, req.username, req.password); //Accept header xhr.setRequestHeader('Accept', resType && MIME[resType] ? MIME[resType] + ',*/*;q=0.01' : '*/*'); //Request mime type if (reqType) xhr.setRequestHeader('Content-Type', reqType); // For same-domain requests, won't change header if already provided. if (!headers['X-Requested-With']) headers['X-Requested-With'] = "XMLHttpRequest"; // The XMLHttpRequest method overrideMimeType() specifies a MIME type other than the one provided by the server to be used instead when interpreting the data being transferred in a request. if (req.overrideResponseMime && xhr.overrideMimeType) xhr.overrideMimeType(req.overrideResponseMime); // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if (req.ifModified) { if (CACHE.lastModified[oURL]) xhr.setRequestHeader('If-Modified-Since', CACHE.lastModified[oURL]); if (CACHE.etag[oURL]) xhr.setRequestHeader('If-None-Match', CACHE.etag[oURL]); } // Set headers for (let h in headers) xhr.setRequestHeader(h, headers[h]); if (req.progress) xhr.onprogress = function (e) { req.progress(e, xhr) }; xhr.onerror = (e) => { _error.call(this, req, xhr, 'error') }; xhr.withCredentials = req.crossCookie; //改造abort函数 let oAbort = xhr.abort; xhr.abort = function () { _error.call(this, req, xhr, xhr['_isTimeout'] ? 'timeout' : 'abort'); oAbort.call(this) } if (req.async) { //同步下不可以设置responseType,否则浏览器抛出异常 xhr.responseType = (resType == 'html' || resType == 'xml') ? 'document' : resType; xhr.timeout = req.timeout || 0; xhr.ontimeout = () => { _error.call(this, req, xhr, 'timeout') }; xhr.onreadystatechange = () => { //4 is DONE, compatible with IE if (xhr.readyState == 4 && xhr.status > 0) _done.call(this, oURL, req, xhr) } } //如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null let data = null; if (req.method != 'HEAD' && req.method != 'GET') { data = Y.isJsonObject(req.data) ? J.stringify(req.data) : req.data; } try { //早期浏览器的timeout是无效,自己实现超时取消 if (req.async && req.timeout > 0) { var timer = self.setTimeout(function () { xhr['_isTimeout'] = true; xhr.abort(); self.clearTimeout(timer); }, req.timeout); } xhr['timestamp'] = new Date().getTime();//记录发送的时间戳,可能在进度回调时调用 xhr.send(data); } catch (e) { _error.call(this, req, xhr, 'error') } if (!req.async && xhr.status > 0) _done.call(this, oURL, req, xhr); } /** * HTTP类 */ export class Http { public static toRequest(quy: string | HttpRequest): HttpRequest { return Y.isString(quy) ? <HttpRequest>{ url: <string>quy } : <HttpRequest>quy; } /** * Send an ajax request. */ static send(req: HttpRequest | URLString) { let q = this.toRequest(req); return q.thread ? this._inThread(req) : this._inMain(req) } /** * Send an ajax request in main thread. */ private static _inMain(req: HttpRequest | URLString) { return Promises.create<HttpResponse>(function () { _send.call(this, req) }) } /** * Send a GET request. */ static get(req: HttpRequest | URLString) { let r: HttpRequest = this.toRequest(req); r.method = 'GET'; return this.send(r) } /** * Send a POST request. */ static post(req: HttpRequest | URLString) { let r: HttpRequest = this.toRequest(req); r.method = 'POST'; return this.send(r) } /** * Upload a file blob. */ static upload(file: { data: Blob, postName?: string, fileName?: string } | FormData, url: URLString) { let fm: FormData; if (file instanceof FormData) { fm = file; } else { fm = new FormData(); fm.append(file.postName || 'file', file.data, file.fileName); } return this.send({ url: url, method: 'POST', data: fm, requestMime: 'multipart/form-data' }) } static _ON: JsonObject<(res: HttpResponse) => void> = {}; /** * A function to be called when any request finishes (after success and error callbacks are executed). */ static on(ev: 'complete', fn: (res: HttpResponse) => void) /** * A function to be called if any request succeeds. */ static on(ev: 'success', fn: (res: HttpResponse) => void) /** * A function to be called if any request fails. */ static on(ev: 'error', fn: (res: HttpResponse) => void) static on(ev: string, fn: (res: HttpResponse) => void) { this._ON[ev] = fn } /** * Allows data to be sent asynchronously to a server with navigator.sendBeacon, even after a page was closed. * Useful for posting analytics data the moment a user was finished using the page. */ static sendBeacon(e: 'beforeunload' | 'unload', fn: (evt: Event) => void, scope?: any) { window.addEventListener('unload', scope ? fn : function (e) { fn.call(scope, e) }, false); } /** * Send an ajax request in a new webwork. */ private static _inThread(req: HttpRequest | URLString) { let r: HttpRequest = this.toRequest(req); r.url = URI.toAbsoluteURL(r.url); return Promises.create<HttpResponse>(function () { let ctx = this; new Thread({ run: function () { this.onposted((request) => { (<any>self).Http._inMain(request).then((res) => { delete res.xhr; this.postMain(res); }) }) } }, typeof r.thread === 'boolean' ? null : r.thread).on('message', function (e, res: HttpResponse) { ctx.resolve(res); this.terminate(); }).start().postThread(r); }) } } } } import Http = JS.net.Http; import HttpRequest = JS.net.HttpRequest; import HttpResponse = JS.net.HttpResponse;