UNPKG

fet-block

Version:

fetBlock is a web h5 request hook library

536 lines (508 loc) 16.2 kB
/*! * 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(); }));