fet-block
Version:
fetBlock is a web h5 request hook library
536 lines (508 loc) • 16.2 kB
JavaScript
/*!
* fet-block v1.0.0
* (c) sheldon cui
* Released under the MIT License.
*/
(function (factory) {
typeof define === 'function' && define.amd ? define(factory) :
factory();
})((function () { 'use strict';
// 技巧:自杀式函数状态记录。
let getPrototypeOf = function (obj) {
if (Object.setPrototypeOf) {
getPrototypeOf = Object.getPrototypeOf;
} else {
getPrototypeOf = function (obj) {
return obj.__proto__;
};
}
return getPrototypeOf(obj);
};
function getNativeXhr() {
let xhrProto = window.XMLHttpRequest.prototype;
while (xhrProto && getPrototypeOf(xhrProto) !== XMLHttpRequestEventTarget.prototype) {
xhrProto = getPrototypeOf(xhrProto);
}
return xhrProto.constructor;
}
const nativeXMLHttpRequest = getNativeXhr();
// 使用原生
async function nativeXhrRequest(options) {
return new Promise(resolve => {
const xhr = new nativeXMLHttpRequest();
xhr.openUrl = options.url;
xhr.open(options.method || 'get', options.url);
xhr.timeout = options.timeout || 5000;
xhr.responseType = options.responseType || 'text';
xhr.onload = function () {
if (xhr.responseType === 'text') {
resolve({
status: xhr.status,
statusText: xhr.statusText,
body: xhr.responseText,
headers: xhr.getAllResponseHeaders()
});
} else {
resolve(xhr.response);
}
};
xhr.onerror = function (e) {
reject(new Error('network error'));
};
xhr.ontimeout = function (e) {
reject(new Error('network timeout'));
};
xhr.send();
});
}
function toAbsoluteUrl(url) {
const isAbsoluteUrl = /^([a-z]+:)?\/\//i.test(url);
if (isAbsoluteUrl) return url;
const currentURL = new URL(window.location.href);
const currentBaseURL = currentURL.origin + currentURL.pathname;
const absoluteTargetUrl = new URL(url, currentBaseURL);
return absoluteTargetUrl.toString();
}
function standardizationRequestOptions(reqOptions) {
const {
url = '',
method = 'get',
headers = {},
body
} = reqOptions;
const newHeaders = {};
for (const key in headers) {
newHeaders[key.toLowerCase()] = headers[key];
}
return {
url: toAbsoluteUrl(url),
method: method.toLowerCase(),
headers: newHeaders || {},
body
};
}
// 执行script脚本
function getLocalStorageObject(localStorageKey) {
try {
const configStr = localStorage.getItem(localStorageKey);
if (configStr) {
return JSON.parse(configStr);
}
} catch (e) {
return null;
}
}
function getUAInfo() {
const ua = navigator.userAgent;
const isAndroid = /Android/.test(ua);
const isIOS = /iPhone|iPad|iPod/.test(ua);
const isWeixin = /MicroMessenger/.test(ua);
const isQQ = /QQ/.test(ua);
const isPC = !isAndroid && !isIOS;
const lang = window.navigator.language;
return {
isAndroid,
isIOS,
isWeixin,
isQQ,
isPC,
lang
};
}
function getUrlDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (err) {
return '';
}
}
function getAllUrlParams(url) {
const queryStr = url.split('#')[0].split('?')[1] || '';
const obj = {};
queryStr.split('&').forEach(item => {
const [key, value] = item.split('=');
obj[key] = value;
});
return obj;
}
function getUrlParams(url, name) {
return getAllUrlParams(url)[name];
}
// 调用 sendRequestByJsBridge 这个jsbridge发送http请求
function sendRequestByJsBridge(reqOptions) {
// 此处我们用原生xhr模拟
return nativeXhrRequest(reqOptions);
}
function caniuse() {
return true;
}
// 实现jsbridge调用能力
// 根据通道类型,调用对应的通道api发请求
/**
* Sends a request using the specified tunnel type.
*
* @param {Object} reqOptions - The options for the request.
* @param {string} tunnelApiName - The type of tunnel to use for sending the request.
* @returns {Promise} A promise that resolves with the response from the request.
*
* @example
* sendRequestByTunnel({ url: 'https://example.com' }, 'sendRequestByJsBridge');
*/
async function sendRequestByTunnel(reqOptions, tunnelApiName) {
switch (tunnelApiName) {
case 'sendRequestByJsBridge':
// 调用webview通道api发请求
return sendRequestByJsBridge(reqOptions);
}
}
/**
* Checks if the specified tunnel type can be used.
* @param {string} tunnelType - The type of tunnel to check.
* @returns {boolean} - True if the tunnel type can be used, otherwise false.
*/
function isCanIUseThisTunnel(tunnelType) {
if (tunnelType === 'sendRequestByJsBridge') {
return caniuse();
}
return false;
}
function getAllTunnelTypes() {
return ['sendRequestByJsBridge'];
}
function printDebugLog(...args) {
const isDebug = getUrlParams(window.location.href, 'fetblock_debug');
if (isDebug) {
console.warn(...args);
}
}
let configJsonUrl = '/dist/config.json';
if (window.fetBlockConfigUrl) configJsonUrl = window.fetBlockConfigUrl;
const LOCAL_STORAGE_FET_BLOCK_CONFIG_KEY = 'fet_block_config_json';
let isFirstInitFlag = true;
const FET_BLOCK_CONFIG = {
originConfig: {},
userProperty: {},
finalRules: {}
};
// 获取该用户属性所映射出的最终配置,且结构为 domain: [rule, rule]
function getConfigByUser(originConfig, userProperty) {
const configMap = originConfig[userProperty.lang];
for (const key in configMap) {
if (configMap[key] && typeof configMap[key] === 'string') {
configMap[key] = [configMap[key]];
}
}
return configMap;
}
async function init$1() {
printDebugLog('init rulesManager');
// 1. 拿出本地config配置
const config = getLocalStorageObject(LOCAL_STORAGE_FET_BLOCK_CONFIG_KEY);
if (config) {
FET_BLOCK_CONFIG.originConfig = config;
}
// 2. 计算出用户自身属性
FET_BLOCK_CONFIG.userProperty = {
lang: getUAInfo().lang
};
// 3.计算出当前用户属性所对应的最终配置
// 若配置支持运营商/特殊uid等粒度,则需要对配置进行merge处理。此处暂时省略。
// 这里暂且只做一个 country 级别的计算
FET_BLOCK_CONFIG.finalRules = getConfigByUser(FET_BLOCK_CONFIG.originConfig, FET_BLOCK_CONFIG.userProperty) || {};
window.FET_BLOCK_CONFIG = FET_BLOCK_CONFIG;
// 4. 异步拉取远程最新配置
if (isFirstInitFlag) {
isFirstInitFlag = false;
const res = await nativeXhrRequest({
url: configJsonUrl
});
localStorage.setItem(LOCAL_STORAGE_FET_BLOCK_CONFIG_KEY, res?.body);
init$1();
}
}
// 根据请求参数,找出该url命中的最佳规则. 最佳规则结构:{type: 'tunnel' | 'xhr', targetUrl: string}
function getRuleByRequestUrl(reqOptions, isForceXHR) {
const standardizationReqOptions = standardizationRequestOptions(reqOptions);
const {
finalRules
} = FET_BLOCK_CONFIG;
// 1. 基于域名找出规则数组
const domain = getUrlDomain(standardizationReqOptions?.url);
const domainRules = finalRules[domain];
if (!domainRules) {
return {
type: 'xhr',
targetUrl: standardizationReqOptions?.url
};
}
// 2. 遍历所有规则,找到第一个匹配的返回
let bestRule = null;
for (const rule of domainRules) {
if (getAllTunnelTypes().includes(rule)) {
if (isCanIUseThisTunnel(rule) && !isForceXHR) {
bestRule = {
type: 'tunnel',
targetUrl: standardizationReqOptions?.url,
tunnelApi: rule
};
} else {
continue;
}
} else {
bestRule = {
type: 'xhr',
targetUrl: standardizationReqOptions?.url?.replace(domain, rule)
};
}
return bestRule;
}
}
function getDomainReplaceRuleByRequestUrl(reqOptions) {
return getRuleByRequestUrl(reqOptions, true);
}
// 实现fetBlock封装后的 XMLHttpRequset class
class FetBlockXMLHttpRequest extends window.XMLHttpRequest {
// 静态属性
static isFetBlockXHR = true;
static fetBlockVersion = "1.0.0";
// 实例属性
#status = 0; // 模拟xhr http报文响应状态
#statusText = ''; // 模拟xhr http报文响应文本
#readyState = 0; // 模拟xhr响应进度状态
#responseText = ''; // 模拟xhr响应内容
#responseHeaders = {}; // 模拟xhr响应头
#responseURL = ''; // 模拟xhr响应url
// fetBlockXHR内部私有变量
#tunnelType = ''; // 模拟xhr请求类型
#isAbort = false; // 标记是否取消此次请求
#requestHeaders = {}; // 存储xhr请求头
#openArgs = null; // 存储open时候的参数
constructor() {
super();
}
get status() {
if (this.#tunnelType !== 'xhr') return this.#status;
return super.status;
}
get statusText() {
if (this.#tunnelType !== 'xhr') return this.#statusText;
return super.statusText;
}
get readyState() {
if (this.#tunnelType !== 'xhr') return this.#readyState;
return super.readyState;
}
get responseText() {
if (this.#tunnelType !== 'xhr') return this.#responseText;
return super.responseText;
}
get responseHeaders() {
if (this.#tunnelType !== 'xhr') return this.#responseHeaders;
return super.responseHeaders;
}
get responseURL() {
if (this.#tunnelType !== 'xhr') return this.#responseURL;
return super.responseURL;
}
get response() {
if (this.#tunnelType !== 'xhr') {
if (this.responseType === 'json') {
return JSON.parse(this.#responseText);
} else {
return this.#responseText;
}
}
return super.response;
}
open(method, url, async = true, username, password) {
this.#resetXhrStatus();
if (!method || !url) {
throw new Error('method and url are required');
}
// 无论走什么通道,都要先用原生xhr open一下,因为open之后,才能调用setRequestHeader这类api
super.open(method, url, async, username, password);
this.#openArgs = {
method,
url,
async,
username,
password
};
// 模拟原生行为,更新readyState
this.#readyState = 1;
}
send(bodyData) {
const bestRule = getRuleByRequestUrl(this.#openArgs);
printDebugLog('最佳规则为:', bestRule);
if (!bestRule) return super.send();
this.#tunnelType = bestRule.type;
// 原始xhr通道
if (this.#tunnelType !== 'tunnel') {
// 由于替换过了域名,因此需要再次open一次
super.open(this.#openArgs.method, bestRule.targetUrl, this.#openArgs.async, this.#openArgs.username, this.#openArgs.password);
Object.keys(this.#requestHeaders).forEach(k => {
super.setRequestHeader(k, this.#requestHeaders[k]);
});
return super.send(bodyData);
}
// 特殊通道则调用特殊通道来完成请求
const tunnelApi = bestRule.tunnelApi;
const requsetPromise = sendRequestByTunnel({
...this.#openArgs,
url: bestRule.targetUrl,
body: bodyData
}, tunnelApi);
this.dispatchEvent(new ProgressEvent('loadstart', {
loaded: 0,
total: 0
}));
requsetPromise.then(res => {
if (this.#isAbort) return;
let loaded = 0;
let total = 100;
if (typeof res === 'object' && res?.body && typeof res?.body === 'string') {
loaded = res?.body?.length;
total = res?.body?.length;
}
this.dispatchEvent(new ProgressEvent('progress', {
loaded,
total
}));
this.#readyState = 4;
this.#status = res.status || 200;
this.#statusText = res.statusText || 'TUNNEL_REQUEST_OK';
this.#responseHeaders = res?.headers || {};
this.#responseText = res?.body;
this.#responseURL = bestRule.targetUrl;
this.dispatchEvent(new ProgressEvent('readystatechange'));
this.dispatchEvent(new ProgressEvent('load', {
loaded: res?.body?.length,
total: res?.body?.length
}));
printDebugLog('xhr内部onload触发完成');
}).catch(err => {
if (this.#isAbort) return;
this.#resetXhrStatus({
status: 0,
statusText: '',
readyState: 4
});
this.#responseText = '';
this.#responseHeaders = {};
this.#responseURL = '';
this.dispatchEvent(new ProgressEvent('error', {
loaded: 0,
total: 0
}));
this.dispatchEvent(new ProgressEvent('loadend', {
loaded: 0,
total: 0
}));
return err;
}).then(res => {
if (this.#isAbort) return;
let loaded = 0;
let total = 100;
if (typeof res === 'object' && res?.body && typeof res?.body === 'string') {
loaded = res?.body?.length;
total = res?.body?.length;
}
this.dispatchEvent(new ProgressEvent('loadend', {
loaded,
total
}));
});
// 发完tunnel请求后,立刻模拟readyState状态
this.#readyState = 2;
this.dispatchEvent(new Event('readystatechange'));
}
getAllResponseHeaders() {
if (this.#tunnelType !== 'xhr') return getResponseHeadersStr(this.#responseHeaders);
return super.getAllResponseHeaders();
}
getResponseHeader(name) {
if (this.#tunnelType !== 'xhr') return this.#responseHeaders[name];
return super.getResponseHeader(name);
}
setRequestHeader(name, value) {
this.#requestHeaders[name] = value;
super.setRequestHeader(name, value);
}
abort() {
this.#isAbort = true;
super.abort();
this.#resetXhrStatus({
isAbort: true
});
if (this.#tunnelType !== 'xhr') {
Promise.resolve().then(() => {
this.dispatchEvent(new ProgressEvent('abort', {
loaded: 0,
total: 0
}));
});
}
}
#resetXhrStatus(resetParams = {}) {
const {
readyState,
status,
statusText,
isAbort
} = resetParams;
this.#status = status || 0;
this.#statusText = statusText;
this.#readyState = readyState || 0;
this.#requestHeaders = {};
this.#responseText = '';
this.#responseHeaders = {};
this.#responseURL = '';
this.#tunnelType = 'xhr';
this.#isAbort = typeof isAbort === 'boolean' ? isAbort : false;
}
}
// 替换url中的域名部分
function fetBlockReplaceUrl(url) {
const bestXHRRule = getDomainReplaceRuleByRequestUrl({
url
});
if (bestXHRRule) {
return bestXHRRule.targetUrl;
}
return url;
}
function hookXMLHttpRequest() {
if (!window.XMLHttpRequest?.isFetBlockXHR) {
window.XMLHttpRequest = FetBlockXMLHttpRequest;
}
}
function hookFetch() {
if (!window.fetch) return;
const originFetch = window.fetch;
window.fetch = function (resource, ...rest) {
if (typeof resource === 'string') {
return originFetch.call(this, fetBlockReplaceUrl(resource), ...rest);
}
return originFetch.call(this, resource, ...rest);
};
}
function init() {
printDebugLog('init hookmanager');
hookXMLHttpRequest();
printDebugLog('hook xhr');
hookFetch();
printDebugLog('hook fetch');
}
function startFetBlock() {
// 0. 防止2次初始化
if (window._isInitFetBlock) return;
// 1. 初始化用户规则
init$1();
// 2. 初始化用户拦截
init();
// 3. 标记初始化完成
window._isInitFetBlock = true;
}
startFetBlock();
}));