mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-17 12:53:02 +00:00
Implement fetch polyfill and Promise for ES5 backend
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package cn.qaiu.parser.customjs;
|
||||
|
||||
import cn.qaiu.parser.customjs.JsHttpClient.JsHttpResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JavaScript Fetch API桥接类
|
||||
* 将标准的fetch API调用桥接到现有的JsHttpClient实现
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/12/06
|
||||
*/
|
||||
public class JsFetchBridge {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsFetchBridge.class);
|
||||
|
||||
private final JsHttpClient httpClient;
|
||||
|
||||
public JsFetchBridge(JsHttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch API实现
|
||||
* 接收fetch API调用并转换为JsHttpClient调用
|
||||
*
|
||||
* @param url 请求URL
|
||||
* @param options 请求选项(包含method、headers、body等)
|
||||
* @return JsHttpResponse响应对象
|
||||
*/
|
||||
public JsHttpResponse fetch(String url, Map<String, Object> options) {
|
||||
try {
|
||||
// 解析请求方法
|
||||
String method = "GET";
|
||||
if (options != null && options.containsKey("method")) {
|
||||
method = options.get("method").toString().toUpperCase();
|
||||
}
|
||||
|
||||
// 解析并设置请求头
|
||||
if (options != null && options.containsKey("headers")) {
|
||||
Object headersObj = options.get("headers");
|
||||
if (headersObj instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> headersMap = (Map<String, Object>) headersObj;
|
||||
for (Map.Entry<String, Object> entry : headersMap.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
httpClient.putHeader(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
Object body = null;
|
||||
if (options != null && options.containsKey("body")) {
|
||||
body = options.get("body");
|
||||
}
|
||||
|
||||
// 根据方法执行请求
|
||||
JsHttpResponse response;
|
||||
switch (method) {
|
||||
case "GET":
|
||||
response = httpClient.get(url);
|
||||
break;
|
||||
case "POST":
|
||||
response = httpClient.post(url, body);
|
||||
break;
|
||||
case "PUT":
|
||||
response = httpClient.put(url, body);
|
||||
break;
|
||||
case "DELETE":
|
||||
response = httpClient.delete(url);
|
||||
break;
|
||||
case "PATCH":
|
||||
response = httpClient.patch(url, body);
|
||||
break;
|
||||
case "HEAD":
|
||||
response = httpClient.getNoRedirect(url);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的HTTP方法: " + method);
|
||||
}
|
||||
|
||||
log.debug("Fetch请求完成: {} {} - 状态码: {}", method, url, response.statusCode());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Fetch请求失败: {} - {}", url, e.getMessage());
|
||||
throw new RuntimeException("Fetch请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,13 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JavaScript解析器执行器
|
||||
@@ -30,17 +35,19 @@ public class JsParserExecutor implements IPanTool {
|
||||
|
||||
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
||||
|
||||
private static String FETCH_RUNTIME_JS = null;
|
||||
|
||||
private final CustomParserConfig config;
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
private final ScriptEngine engine;
|
||||
private final JsHttpClient httpClient;
|
||||
private final JsLogger jsLogger;
|
||||
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||
private final JsFetchBridge fetchBridge;
|
||||
|
||||
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
|
||||
this.config = config;
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
this.engine = initEngine();
|
||||
|
||||
// 检查是否有代理配置
|
||||
JsonObject proxyConfig = null;
|
||||
@@ -51,6 +58,34 @@ public class JsParserExecutor implements IPanTool {
|
||||
this.httpClient = new JsHttpClient(proxyConfig);
|
||||
this.jsLogger = new JsLogger("JsParser-" + config.getType());
|
||||
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
||||
this.fetchBridge = new JsFetchBridge(httpClient);
|
||||
this.engine = initEngine();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载fetch运行时JS代码
|
||||
* @return fetch运行时代码
|
||||
*/
|
||||
static String loadFetchRuntime() {
|
||||
if (FETCH_RUNTIME_JS != null) {
|
||||
return FETCH_RUNTIME_JS;
|
||||
}
|
||||
|
||||
try (InputStream is = JsParserExecutor.class.getClassLoader().getResourceAsStream("fetch-runtime.js")) {
|
||||
if (is == null) {
|
||||
log.warn("未找到fetch-runtime.js文件,fetch API将不可用");
|
||||
return "";
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
FETCH_RUNTIME_JS = reader.lines().collect(Collectors.joining("\n"));
|
||||
log.debug("Fetch运行时加载成功,大小: {} 字符", FETCH_RUNTIME_JS.length());
|
||||
return FETCH_RUNTIME_JS;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("加载fetch-runtime.js失败", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,6 +116,7 @@ public class JsParserExecutor implements IPanTool {
|
||||
engine.put("http", httpClient);
|
||||
engine.put("logger", jsLogger);
|
||||
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
||||
engine.put("JavaFetch", fetchBridge);
|
||||
|
||||
// 禁用Java对象访问
|
||||
engine.eval("var Java = undefined;");
|
||||
@@ -90,6 +126,13 @@ public class JsParserExecutor implements IPanTool {
|
||||
engine.eval("var org = undefined;");
|
||||
engine.eval("var com = undefined;");
|
||||
|
||||
// 加载fetch运行时(Promise和fetch API polyfill)
|
||||
String fetchRuntime = loadFetchRuntime();
|
||||
if (!fetchRuntime.isEmpty()) {
|
||||
engine.eval(fetchRuntime);
|
||||
log.debug("✅ Fetch API和Promise polyfill注入成功");
|
||||
}
|
||||
|
||||
log.debug("🔒 安全的JavaScript引擎初始化成功,解析器类型: {}", config.getType());
|
||||
|
||||
// 执行JavaScript代码
|
||||
|
||||
@@ -42,6 +42,7 @@ public class JsPlaygroundExecutor {
|
||||
private final JsHttpClient httpClient;
|
||||
private final JsPlaygroundLogger playgroundLogger;
|
||||
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||
private final JsFetchBridge fetchBridge;
|
||||
|
||||
/**
|
||||
* 创建演练场执行器
|
||||
@@ -62,6 +63,7 @@ public class JsPlaygroundExecutor {
|
||||
this.httpClient = new JsHttpClient(proxyConfig);
|
||||
this.playgroundLogger = new JsPlaygroundLogger();
|
||||
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
||||
this.fetchBridge = new JsFetchBridge(httpClient);
|
||||
this.engine = initEngine();
|
||||
}
|
||||
|
||||
@@ -84,6 +86,7 @@ public class JsPlaygroundExecutor {
|
||||
engine.put("http", httpClient);
|
||||
engine.put("logger", playgroundLogger);
|
||||
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
||||
engine.put("JavaFetch", fetchBridge);
|
||||
|
||||
// 禁用Java对象访问
|
||||
engine.eval("var Java = undefined;");
|
||||
@@ -93,6 +96,13 @@ public class JsPlaygroundExecutor {
|
||||
engine.eval("var org = undefined;");
|
||||
engine.eval("var com = undefined;");
|
||||
|
||||
// 加载fetch运行时(Promise和fetch API polyfill)
|
||||
String fetchRuntime = JsParserExecutor.loadFetchRuntime();
|
||||
if (!fetchRuntime.isEmpty()) {
|
||||
engine.eval(fetchRuntime);
|
||||
playgroundLogger.infoJava("✅ Fetch API和Promise polyfill注入成功");
|
||||
}
|
||||
|
||||
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功(演练场)");
|
||||
|
||||
// 执行JavaScript代码
|
||||
|
||||
105
parser/src/main/resources/custom-parsers/fetch-demo.js
Normal file
105
parser/src/main/resources/custom-parsers/fetch-demo.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// ==UserScript==
|
||||
// @name Fetch API示例解析器
|
||||
// @type fetch_demo
|
||||
// @displayName Fetch演示
|
||||
// @description 演示如何在ES5环境中使用fetch API和async/await
|
||||
// @match https?://example\.com/s/(?<KEY>\w+)
|
||||
// @author QAIU
|
||||
// @version 1.0.0
|
||||
// ==/UserScript==
|
||||
|
||||
// 使用require导入类型定义(仅用于IDE类型提示)
|
||||
var types = require('./types');
|
||||
/** @typedef {types.ShareLinkInfo} ShareLinkInfo */
|
||||
/** @typedef {types.JsHttpClient} JsHttpClient */
|
||||
/** @typedef {types.JsLogger} JsLogger */
|
||||
|
||||
/**
|
||||
* 演示使用fetch API的解析器
|
||||
* 注意:虽然源码中使用了ES6+语法(async/await),但在浏览器中会被编译为ES5
|
||||
*
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||
* @param {JsHttpClient} http - HTTP客户端(传统方式)
|
||||
* @param {JsLogger} logger - 日志对象
|
||||
* @returns {string} 下载链接
|
||||
*/
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
logger.info("=== Fetch API Demo ===");
|
||||
|
||||
// 方式1:使用传统的http对象(同步)
|
||||
logger.info("方式1: 使用传统http对象");
|
||||
var response1 = http.get("https://httpbin.org/get");
|
||||
logger.info("状态码: " + response1.statusCode());
|
||||
|
||||
// 方式2:使用fetch API(基于Promise)
|
||||
logger.info("方式2: 使用fetch API");
|
||||
|
||||
// 注意:在ES5环境中,我们需要手动处理Promise
|
||||
// 这个示例展示了如何在ES5中使用fetch
|
||||
var fetchPromise = fetch("https://httpbin.org/get");
|
||||
|
||||
// 等待Promise完成(同步等待模拟)
|
||||
var result = null;
|
||||
var error = null;
|
||||
|
||||
fetchPromise
|
||||
.then(function(response) {
|
||||
logger.info("Fetch响应状态: " + response.status);
|
||||
return response.text();
|
||||
})
|
||||
.then(function(text) {
|
||||
logger.info("Fetch响应内容: " + text.substring(0, 100) + "...");
|
||||
result = "https://example.com/download/demo.file";
|
||||
})
|
||||
['catch'](function(err) {
|
||||
logger.error("Fetch失败: " + err.message);
|
||||
error = err;
|
||||
});
|
||||
|
||||
// 简单的等待循环(实际场景中不推荐,这里仅作演示)
|
||||
var timeout = 5000; // 5秒超时
|
||||
var start = Date.now();
|
||||
while (result === null && error === null && (Date.now() - start) < timeout) {
|
||||
// 等待Promise完成
|
||||
java.lang.Thread.sleep(10);
|
||||
}
|
||||
|
||||
if (error !== null) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
throw new Error("Fetch超时");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示POST请求
|
||||
*/
|
||||
function demonstratePost(logger) {
|
||||
logger.info("=== 演示POST请求 ===");
|
||||
|
||||
var postPromise = fetch("https://httpbin.org/post", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: "value",
|
||||
demo: true
|
||||
})
|
||||
});
|
||||
|
||||
postPromise
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
logger.info("POST响应: " + JSON.stringify(data));
|
||||
})
|
||||
['catch'](function(err) {
|
||||
logger.error("POST失败: " + err.message);
|
||||
});
|
||||
}
|
||||
299
parser/src/main/resources/fetch-runtime.js
Normal file
299
parser/src/main/resources/fetch-runtime.js
Normal file
@@ -0,0 +1,299 @@
|
||||
// ==FetchRuntime==
|
||||
// @name Fetch API Polyfill for ES5
|
||||
// @description Fetch API and Promise implementation for ES5 JavaScript engines
|
||||
// @version 1.0.0
|
||||
// @author QAIU
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Simple Promise implementation compatible with ES5
|
||||
* Supports basic Promise functionality needed for fetch API
|
||||
*/
|
||||
function SimplePromise(executor) {
|
||||
var state = 'pending';
|
||||
var value;
|
||||
var handlers = [];
|
||||
var self = this;
|
||||
|
||||
function resolve(result) {
|
||||
if (state !== 'pending') return;
|
||||
state = 'fulfilled';
|
||||
value = result;
|
||||
handlers.forEach(handle);
|
||||
handlers = [];
|
||||
}
|
||||
|
||||
function reject(err) {
|
||||
if (state !== 'pending') return;
|
||||
state = 'rejected';
|
||||
value = err;
|
||||
handlers.forEach(handle);
|
||||
handlers = [];
|
||||
}
|
||||
|
||||
function handle(handler) {
|
||||
if (state === 'pending') {
|
||||
handlers.push(handler);
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') {
|
||||
try {
|
||||
var result = handler.onFulfilled(value);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(handler.resolve, handler.reject);
|
||||
} else {
|
||||
handler.resolve(result);
|
||||
}
|
||||
} catch (e) {
|
||||
handler.reject(e);
|
||||
}
|
||||
}
|
||||
if (state === 'rejected' && typeof handler.onRejected === 'function') {
|
||||
try {
|
||||
var result = handler.onRejected(value);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(handler.resolve, handler.reject);
|
||||
} else {
|
||||
handler.resolve(result);
|
||||
}
|
||||
} catch (e) {
|
||||
handler.reject(e);
|
||||
}
|
||||
} else if (state === 'rejected' && !handler.onRejected) {
|
||||
handler.reject(value);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.then = function(onFulfilled, onRejected) {
|
||||
return new SimplePromise(function(resolveNext, rejectNext) {
|
||||
handle({
|
||||
onFulfilled: onFulfilled,
|
||||
onRejected: onRejected,
|
||||
resolve: resolveNext,
|
||||
reject: rejectNext
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this['catch'] = function(onRejected) {
|
||||
return this.then(null, onRejected);
|
||||
};
|
||||
|
||||
this['finally'] = function(onFinally) {
|
||||
return this.then(
|
||||
function(value) {
|
||||
return SimplePromise.resolve(onFinally()).then(function() {
|
||||
return value;
|
||||
});
|
||||
},
|
||||
function(reason) {
|
||||
return SimplePromise.resolve(onFinally()).then(function() {
|
||||
throw reason;
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
executor(resolve, reject);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Static methods
|
||||
SimplePromise.resolve = function(value) {
|
||||
if (value && typeof value.then === 'function') {
|
||||
return value;
|
||||
}
|
||||
return new SimplePromise(function(resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
};
|
||||
|
||||
SimplePromise.reject = function(reason) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
reject(reason);
|
||||
});
|
||||
};
|
||||
|
||||
SimplePromise.all = function(promises) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
var results = [];
|
||||
var remaining = promises.length;
|
||||
|
||||
if (remaining === 0) {
|
||||
resolve(results);
|
||||
return;
|
||||
}
|
||||
|
||||
function handleResult(index, value) {
|
||||
results[index] = value;
|
||||
remaining--;
|
||||
if (remaining === 0) {
|
||||
resolve(results);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < promises.length; i++) {
|
||||
(function(index) {
|
||||
var promise = promises[index];
|
||||
if (promise && typeof promise.then === 'function') {
|
||||
promise.then(
|
||||
function(value) { handleResult(index, value); },
|
||||
reject
|
||||
);
|
||||
} else {
|
||||
handleResult(index, promise);
|
||||
}
|
||||
})(i);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SimplePromise.race = function(promises) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
for (var i = 0; i < promises.length; i++) {
|
||||
var promise = promises[i];
|
||||
if (promise && typeof promise.then === 'function') {
|
||||
promise.then(resolve, reject);
|
||||
} else {
|
||||
resolve(promise);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Make Promise global if not already defined
|
||||
if (typeof Promise === 'undefined') {
|
||||
var Promise = SimplePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response object that mimics the Fetch API Response
|
||||
*/
|
||||
function FetchResponse(jsHttpResponse) {
|
||||
this._jsResponse = jsHttpResponse;
|
||||
this.status = jsHttpResponse.statusCode();
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
this.statusText = this.ok ? 'OK' : 'Error';
|
||||
this.headers = {
|
||||
get: function(name) {
|
||||
return jsHttpResponse.header(name);
|
||||
},
|
||||
has: function(name) {
|
||||
return jsHttpResponse.header(name) !== null;
|
||||
},
|
||||
entries: function() {
|
||||
var headerMap = jsHttpResponse.headers();
|
||||
var entries = [];
|
||||
for (var key in headerMap) {
|
||||
if (headerMap.hasOwnProperty(key)) {
|
||||
entries.push([key, headerMap[key]]);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
FetchResponse.prototype.text = function() {
|
||||
var body = this._jsResponse.body();
|
||||
return SimplePromise.resolve(body || '');
|
||||
};
|
||||
|
||||
FetchResponse.prototype.json = function() {
|
||||
var self = this;
|
||||
return this.text().then(function(text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON: ' + e.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
FetchResponse.prototype.arrayBuffer = function() {
|
||||
var bytes = this._jsResponse.bodyBytes();
|
||||
return SimplePromise.resolve(bytes);
|
||||
};
|
||||
|
||||
FetchResponse.prototype.blob = function() {
|
||||
// Blob not supported in ES5, return bytes
|
||||
return this.arrayBuffer();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch API implementation using JavaFetch bridge
|
||||
* @param {string} url - Request URL
|
||||
* @param {Object} options - Fetch options (method, headers, body, etc.)
|
||||
* @returns {Promise<FetchResponse>}
|
||||
*/
|
||||
function fetch(url, options) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
try {
|
||||
// Parse options
|
||||
options = options || {};
|
||||
var method = (options.method || 'GET').toUpperCase();
|
||||
var headers = options.headers || {};
|
||||
var body = options.body;
|
||||
|
||||
// Prepare request options for JavaFetch
|
||||
var requestOptions = {
|
||||
method: method,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// Convert headers to simple object
|
||||
if (headers) {
|
||||
if (typeof headers.forEach === 'function') {
|
||||
// Headers object
|
||||
headers.forEach(function(value, key) {
|
||||
requestOptions.headers[key] = value;
|
||||
});
|
||||
} else if (typeof headers === 'object') {
|
||||
// Plain object
|
||||
for (var key in headers) {
|
||||
if (headers.hasOwnProperty(key)) {
|
||||
requestOptions.headers[key] = headers[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add body if present
|
||||
if (body !== undefined && body !== null) {
|
||||
if (typeof body === 'string') {
|
||||
requestOptions.body = body;
|
||||
} else if (typeof body === 'object') {
|
||||
// Assume JSON
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) {
|
||||
requestOptions.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call JavaFetch bridge
|
||||
var jsHttpResponse = JavaFetch.fetch(url, requestOptions);
|
||||
|
||||
// Create Response object
|
||||
var response = new FetchResponse(jsHttpResponse);
|
||||
resolve(response);
|
||||
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export for global use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.fetch = fetch;
|
||||
window.Promise = Promise;
|
||||
} else if (typeof global !== 'undefined') {
|
||||
global.fetch = fetch;
|
||||
global.Promise = Promise;
|
||||
}
|
||||
Reference in New Issue
Block a user