/** * polyfills.js — IE11 / 老浏览器兼容垫片 * ES5 语法,离线内置,无外部依赖。 * * 提供: * String: padStart / startsWith / endsWith / includes * Array: includes * Object: assign * Promise: then / catch / finally / all / race / resolve / reject * fetch: 基于 XMLHttpRequest * EventSource: 基于 XMLHttpRequest,支持 SSE 消息解析和断线重连 * * 注意: * 1. 本文件应在业务 JS 之前加载。 * 2. fetch 和 EventSource 是兼容实现,不保证与原生 API 完全一致。 * 3. downloadBlobFile() 保留在 util.js,不放在这里。 */ (function (global) { "use strict"; /* ========================================================= * String / Array / Object * ========================================================= */ if (!String.prototype.padStart) { String.prototype.padStart = function (targetLength, padString) { var str = String(this); var len = Number(targetLength) || 0; var pad = padString === undefined ? " " : String(padString); var need = len - str.length; var result = ""; if (need <= 0) return str; if (pad === "") return str; while (result.length < need) { result += pad; } return result.slice(0, need) + str; }; } if (!String.prototype.startsWith) { String.prototype.startsWith = function (search, position) { var str = String(this); var pos = Number(position) || 0; search = String(search); if (pos < 0) pos = 0; return str.substr(pos, search.length) === search; }; } if (!String.prototype.endsWith) { String.prototype.endsWith = function (search, length) { var str = String(this); search = String(search); if (length === undefined || length > str.length) { length = str.length; } length = Number(length); if (isNaN(length) || length < 0) length = 0; return str.substring(length - search.length, length) === search; }; } if (!String.prototype.includes) { String.prototype.includes = function (search, start) { if (search instanceof RegExp) { throw new TypeError("不支持将 RegExp 作为 includes 参数"); } return String(this).indexOf(String(search), Number(start) || 0) !== -1; }; } if (!Array.prototype.includes) { Array.prototype.includes = function (search, fromIndex) { var arr = Object(this); var len = arr.length >>> 0; var i = Number(fromIndex) || 0; if (!len) return false; if (i < 0) { i = Math.max(len + i, 0); } for (; i < len; i++) { if (arr[i] === search || (arr[i] !== arr[i] && search !== search)) { return true; } } return false; }; } if (!Object.assign) { Object.assign = function (target) { if (target == null) { throw new TypeError("Object.assign 的目标不能为 null 或 undefined"); } var to = Object(target); var i; var source; var key; for (i = 1; i < arguments.length; i++) { source = arguments[i]; if (source == null) continue; source = Object(source); for (key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { to[key] = source[key]; } } } return to; }; } /* ========================================================= * Promise * ========================================================= */ if (!global.Promise) { var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function flushLater(fn) { setTimeout(fn, 0); } function isObjectOrFunction(value) { return value !== null && (typeof value === "object" || typeof value === "function"); } function settle(promise, state, value) { if (promise._state !== PENDING) return; if (state === FULFILLED && value === promise) { settle(promise, REJECTED, new TypeError("Promise 不能解决为自身")); return; } if (state === FULFILLED && isObjectOrFunction(value)) { var then; var called = false; try { then = value.then; } catch (err) { settle(promise, REJECTED, err); return; } if (typeof then === "function") { try { then.call( value, function (result) { if (called) return; called = true; settle(promise, FULFILLED, result); }, function (reason) { if (called) return; called = true; settle(promise, REJECTED, reason); } ); } catch (err2) { if (!called) { called = true; settle(promise, REJECTED, err2); } } return; } } promise._state = state; promise._value = value; if (promise._handlers.length) { flushLater(function () { flushHandlers(promise); }); } } function flushHandlers(promise) { var handlers = promise._handlers.slice(); var i; var handler; var callback; var result; promise._handlers = []; for (i = 0; i < handlers.length; i++) { handler = handlers[i]; callback = promise._state === FULFILLED ? handler.onFulfilled : handler.onRejected; if (typeof callback !== "function") { if (promise._state === FULFILLED) { settle(handler.next, FULFILLED, promise._value); } else { settle(handler.next, REJECTED, promise._value); } continue; } try { result = callback(promise._value); settle(handler.next, FULFILLED, result); } catch (err) { settle(handler.next, REJECTED, err); } } } function PromisePolyfill(executor) { if (!(this instanceof PromisePolyfill)) { throw new TypeError("Promise 必须通过 new 调用"); } if (typeof executor !== "function") { throw new TypeError("Promise executor 必须是函数"); } this._state = PENDING; this._value = undefined; this._handlers = []; var self = this; var done = false; function resolve(value) { if (done) return; done = true; settle(self, FULFILLED, value); } function reject(reason) { if (done) return; done = true; settle(self, REJECTED, reason); } try { executor(resolve, reject); } catch (err) { reject(err); } } PromisePolyfill.prototype.then = function (onFulfilled, onRejected) { var next = new PromisePolyfill(function () {}); this._handlers.push({ onFulfilled: onFulfilled, onRejected: onRejected, next: next }); if (this._state !== PENDING) { var self = this; flushLater(function () { flushHandlers(self); }); } return next; }; PromisePolyfill.prototype["catch"] = function (onRejected) { return this.then(null, onRejected); }; PromisePolyfill.prototype["finally"] = function (callback) { var P = this.constructor || PromisePolyfill; if (typeof callback !== "function") { return this.then(callback, callback); } return this.then( function (value) { return P.resolve(callback()).then(function () { return value; }); }, function (reason) { return P.resolve(callback()).then(function () { throw reason; }); } ); }; PromisePolyfill.resolve = function (value) { if (value instanceof PromisePolyfill) return value; return new PromisePolyfill(function (resolve) { resolve(value); }); }; PromisePolyfill.reject = function (reason) { return new PromisePolyfill(function (resolve, reject) { reject(reason); }); }; PromisePolyfill.all = function (items) { return new PromisePolyfill(function (resolve, reject) { var list; var results = []; var remaining; var i; try { list = Array.prototype.slice.call(items); } catch (err) { reject(err); return; } if (!list.length) { resolve([]); return; } remaining = list.length; function makeResolver(index) { return function (value) { results[index] = value; remaining--; if (remaining === 0) { resolve(results); } }; } for (i = 0; i < list.length; i++) { PromisePolyfill.resolve(list[i]).then( makeResolver(i), reject ); } }); }; PromisePolyfill.race = function (items) { return new PromisePolyfill(function (resolve, reject) { var list; var i; try { list = Array.prototype.slice.call(items); } catch (err) { reject(err); return; } for (i = 0; i < list.length; i++) { PromisePolyfill.resolve(list[i]).then(resolve, reject); } }); }; global.Promise = PromisePolyfill; } /* ========================================================= * fetch — 基于 XMLHttpRequest * ========================================================= */ if (!global.fetch && global.XMLHttpRequest) { function HeadersPolyfill(headers) { this._headers = {}; var key; var value; var i; if (!headers) return; if (typeof headers.forEach === "function") { var self = this; headers.forEach(function (v, k) { self.append(k, v); }); } else if (Object.prototype.toString.call(headers) === "[object Array]") { for (i = 0; i < headers.length; i++) { if (headers[i] && headers[i].length >= 2) { this.append(headers[i][0], headers[i][1]); } } } else { for (key in headers) { if (Object.prototype.hasOwnProperty.call(headers, key)) { value = headers[key]; this.append(key, value); } } } } HeadersPolyfill.prototype.append = function (name, value) { name = String(name).toLowerCase(); value = String(value); if (this._headers[name]) { this._headers[name] += ", " + value; } else { this._headers[name] = value; } }; HeadersPolyfill.prototype.set = function (name, value) { this._headers[String(name).toLowerCase()] = String(value); }; HeadersPolyfill.prototype.get = function (name) { var value = this._headers[String(name).toLowerCase()]; return value === undefined ? null : value; }; HeadersPolyfill.prototype.has = function (name) { return Object.prototype.hasOwnProperty.call( this._headers, String(name).toLowerCase() ); }; HeadersPolyfill.prototype["delete"] = function (name) { delete this._headers[String(name).toLowerCase()]; }; HeadersPolyfill.prototype.forEach = function (callback, thisArg) { var key; for (key in this._headers) { if (Object.prototype.hasOwnProperty.call(this._headers, key)) { callback.call(thisArg, this._headers[key], key, this); } } }; function parseResponseHeaders(raw) { var headers = new HeadersPolyfill(); var lines = raw ? raw.split(/\r?\n/) : []; var i; var index; for (i = 0; i < lines.length; i++) { index = lines[i].indexOf(":"); if (index > 0) { headers.append( lines[i].substring(0, index).replace(/^\s+|\s+$/g, ""), lines[i].substring(index + 1).replace(/^\s+|\s+$/g, "") ); } } return headers; } function ResponsePolyfill(xhr) { this.status = xhr.status === 1223 ? 204 : xhr.status; this.statusText = xhr.statusText || ""; this.ok = this.status >= 200 && this.status < 300; this.url = xhr.responseURL || ""; this.headers = parseResponseHeaders(xhr.getAllResponseHeaders()); this._xhr = xhr; this._bodyUsed = false; } ResponsePolyfill.prototype.text = function () { var xhr = this._xhr; return Promise.resolve(xhr.responseText || ""); }; ResponsePolyfill.prototype.json = function () { return this.text().then(function (text) { return JSON.parse(text); }); }; ResponsePolyfill.prototype.blob = function () { var xhr = this._xhr; if (xhr.response && typeof xhr.response !== "string") { return Promise.resolve(xhr.response); } if (global.Blob) { return Promise.resolve(new Blob([xhr.responseText || ""], { type: this.headers.get("content-type") || "" })); } return Promise.reject(new Error("当前浏览器不支持 Blob")); }; ResponsePolyfill.prototype.clone = function () { return new ResponsePolyfill(this._xhr); }; global.Headers = global.Headers || HeadersPolyfill; global.Response = global.Response || ResponsePolyfill; global.fetch = function (input, init) { return new Promise(function (resolve, reject) { var options = init || {}; var url = typeof input === "string" ? input : input.url; var method = options.method || "GET"; var xhr = new XMLHttpRequest(); var headers = new HeadersPolyfill(options.headers); var body = options.body === undefined ? null : options.body; var key; var settled = false; function fail(err) { if (settled) return; settled = true; reject(err); } try { xhr.open(method, url, true); if (options.credentials === "include") { xhr.withCredentials = true; } else if (options.credentials === "same-origin") { xhr.withCredentials = false; } if (options.responseType && options.responseType !== "text") { try { xhr.responseType = options.responseType; } catch (ignore) {} } headers.forEach(function (value, name) { xhr.setRequestHeader(name, value); }); xhr.onload = function () { if (settled) return; settled = true; if (xhr.status === 0) { reject(new TypeError("网络请求失败")); return; } resolve(new ResponsePolyfill(xhr)); }; xhr.onerror = function () { fail(new TypeError("网络请求失败")); }; xhr.ontimeout = function () { fail(new TypeError("网络请求超时")); }; xhr.onabort = function () { fail(new TypeError("网络请求已取消")); }; if (options.timeout) { xhr.timeout = options.timeout; } xhr.send(body); } catch (err) { fail(err); } }); }; } /* ========================================================= * EventSource — 基于 XMLHttpRequest 的 SSE 兼容实现 * ========================================================= */ if (!global.EventSource && global.XMLHttpRequest) { function EventSourcePolyfill(url, options) { if (!(this instanceof EventSourcePolyfill)) { throw new TypeError("EventSource 必须通过 new 调用"); } if (!url) { throw new TypeError("EventSource URL 不能为空"); } this.url = String(url); this.withCredentials = !!(options && options.withCredentials); this.CONNECTING = 0; this.OPEN = 1; this.CLOSED = 2; this.readyState = this.CONNECTING; this.onopen = null; this.onmessage = null; this.onerror = null; this._xhr = null; this._timer = null; this._closed = false; this._retry = 3000; this._lastEventId = ""; this._buffer = ""; this._listeners = {}; this._connect(); } EventSourcePolyfill.prototype.addEventListener = function (type, listener) { if (typeof listener !== "function") return; if (!this._listeners[type]) { this._listeners[type] = []; } this._listeners[type].push(listener); }; EventSourcePolyfill.prototype.removeEventListener = function (type, listener) { var list = this._listeners[type]; var i; if (!list) return; for (i = list.length - 1; i >= 0; i--) { if (list[i] === listener) { list.splice(i, 1); } } }; EventSourcePolyfill.prototype.dispatchEvent = function (event) { var list = this._listeners[event.type] || []; var i; if (typeof this["on" + event.type] === "function") { try { this["on" + event.type].call(this, event); } catch (ignore) {} } list = list.slice(); for (i = 0; i < list.length; i++) { try { list[i].call(this, event); } catch (ignore2) {} } return true; }; EventSourcePolyfill.prototype._emit = function (type, data, lastEventId) { var event = { type: type, data: data === undefined ? "" : data, lastEventId: lastEventId || "", origin: "", target: this }; event.preventDefault = function () {}; event.stopPropagation = function () {}; this.dispatchEvent(event); }; EventSourcePolyfill.prototype._connect = function () { var self = this; var xhr; var lastLength = 0; var finished = false; if (this._closed) return; try { xhr = new XMLHttpRequest(); this._xhr = xhr; this._buffer = ""; xhr.open("GET", this.url, true); xhr.setRequestHeader("Accept", "text/event-stream"); xhr.setRequestHeader("Cache-Control", "no-cache"); if (this._lastEventId) { xhr.setRequestHeader("Last-Event-ID", this._lastEventId); } xhr.withCredentials = this.withCredentials; function readData() { var text; var chunk; try { text = xhr.responseText || ""; } catch (ignore) { return; } if (text.length < lastLength) { lastLength = 0; self._buffer = ""; } if (text.length <= lastLength) return; chunk = text.substring(lastLength); lastLength = text.length; self._buffer += chunk; self._parseBuffer(false); } xhr.onreadystatechange = function () { if (self._closed || finished) return; if (xhr.readyState === 2 || xhr.readyState === 3) { if (xhr.status >= 200 && xhr.status < 300) { if (self.readyState !== self.OPEN) { self.readyState = self.OPEN; self._emit("open"); } if (xhr.readyState === 3) { readData(); } } else if (xhr.readyState === 2) { finished = true; self._handleError(); } } if (xhr.readyState === 4) { readData(); if (!self._closed) { finished = true; self._parseBuffer(true); self._handleError(); } } }; xhr.onerror = function () { if (self._closed || finished) return; finished = true; self._handleError(); }; xhr.ontimeout = function () { if (self._closed || finished) return; finished = true; self._handleError(); }; xhr.send(null); } catch (err) { this._handleError(); } }; EventSourcePolyfill.prototype._parseBuffer = function (flush) { var index; var line; var fields; var i; var field; var value; var data = []; var eventType = ""; var eventId = this._lastEventId; var retry = null; while ((index = this._buffer.indexOf("\n")) !== -1) { line = this._buffer.substring(0, index); this._buffer = this._buffer.substring(index + 1); if (line.charAt(line.length - 1) === "\r") { line = line.substring(0, line.length - 1); } if (line === "") { if (data.length) { this._lastEventId = eventId; this._emit( eventType || "message", data.join("\n"), eventId ); } data = []; eventType = ""; eventId = this._lastEventId; continue; } if (line.charAt(0) === ":") continue; fields = line.split(":"); field = fields.shift(); value = fields.join(":"); if (value.charAt(0) === " ") { value = value.substring(1); } if (field === "data") { data.push(value); } else if (field === "event") { eventType = value; } else if (field === "id" && value.indexOf("\0") === -1) { eventId = value; } else if (field === "retry") { if (/^\d+$/.test(value)) { retry = parseInt(value, 10); } } } if (retry !== null && retry >= 0) { this._retry = retry; } if (flush && this._buffer) { line = this._buffer; this._buffer = ""; if (line.charAt(line.length - 1) === "\r") { line = line.substring(0, line.length - 1); } if (line.indexOf(":") !== 0) { fields = line.split(":"); field = fields.shift(); value = fields.join(":"); if (value.charAt(0) === " ") { value = value.substring(1); } if (field === "data") { data.push(value); } else if (field === "event") { eventType = value; } else if (field === "id" && value.indexOf("\0") === -1) { eventId = value; } else if (field === "retry" && /^\d+$/.test(value)) { this._retry = parseInt(value, 10); } } if (data.length) { this._lastEventId = eventId; this._emit( eventType || "message", data.join("\n"), eventId ); } } }; EventSourcePolyfill.prototype._handleError = function () { var self = this; if (this._closed) return; this.readyState = this.CONNECTING; this._emit("error"); if (this._timer) { clearTimeout(this._timer); } this._timer = setTimeout(function () { self._timer = null; self._connect(); }, this._retry); }; EventSourcePolyfill.prototype.close = function () { this._closed = true; this.readyState = this.CLOSED; if (this._timer) { clearTimeout(this._timer); this._timer = null; } if (this._xhr) { try { this._xhr.onreadystatechange = null; this._xhr.onerror = null; this._xhr.abort(); } catch (ignore) {} this._xhr = null; } }; global.EventSource = EventSourcePolyfill; } })(window);