Skip to content

logic之规则生成

所谓规则生成,即 initRulesManager 函数的主逻辑。即如何远程拉取我们的“域名替换规则”以及如何将规则进行计算,得到当前用户最终应当使用的规则---finalRules。

流程图

规则生成,涉及到如下3个核心步骤:

  1. 从ua读取该用户的相关属性,得到用户的关键信息: {lang, country, isp, isSpecial}。 即语言码、国家码、运营商、是否是特殊关照用户。
  2. 将上述4个信息按照优先级依次merge,得到最终finalRules。其中country优先级大于lang,isp优先级大于country,isSpecial优先级大于isp。即从前往后,依次将每份配置merge到前一份配置上。
  3. 最终得到的finalRules会挂载到全局window对象上,以供后续hook拦截相关过程使用。

由于上述第二个步骤存在merge过程,因此我们为了能确保后续hook拦截后能有一定规则匹配灵活度,我们并不会直接用高优的配置覆盖低优的配置,而是会merge成一个数组。

例如原始配置如下:

js
{
    "en": {
        "api.mydemo.com": "bakapi.mydeom.com"
    },
    "en_special": {
        "api.mydemo.com": "sendRequestByJsBridge"
    }
}

那么,这个api.mydemo.com域名在最终finalRules会变成如下结构:

js
{
    "en": [
        "api.mydemo.com": ["sendRequestByJsBridge", "bakapi.mydeom.com"]
    ]
}

由于 en_special 要具有更高优先级,因此其排在数组的第一位,以便后续hook拦截中 xmlHttpRequest拦截到请求后优先使用 sendRequestByJsBridge api。

在浏览器中访问 window.FET_BLOCK_CONFIG

在浏览器运行时,随时随地都可以通过访问 window.FET_BLOCK_CONFIG 来获取到当前用户最终的规则和原始配置信息。示例:

有了这份最终规则,我们就可以在后续的拦截过程中,根据这份规则来决定如何进行拦截。

要注意的是,为了能确保页面运行时尽可能早的初始化这份配置,且同时又希望尽可能让这份配置保持最新。因此这份配置每次页面打开后会“优先用localstorage”那一份,同时会异步拉取一份最新的并在拉取成功后重新更新浏览器运行时中的这份配置和window上的对象,具体可参考下文代码部分。

initRulesManager逻辑代码

以下是 initRulesManager 函数的代码片段,供大家参考。

js
import { nativeXhrRequest, standardizationRequestOptions } from "../../tools/xhr";
import { getUAInfo, getLocalStorageObject } from "../../tools/dom";
import { getUrlDomain } from "../../tools/url";
import { getAllTunnelTypes, isCanIUseThisTunnel } from "../tunnels/index";

let configJsonUrl = process.env.NODE_ENV === 'production'
    ? `https://www.unpkg.com/fet@${pkgVersion}/dist/config.json`
    : '/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
}

export async function init() {
    console.log('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()
    }
}

向外暴露获取最佳规则的函数

由于hook拦截过程中,需要用到我们的 finalRules,且需要能快速从finalRules中找出最佳的匹配规则。因此,我们需对外提供2个快捷函数:

  1. getRuleByRequestUrl。功能是用于根据某次ajax请求的url,依次遍历该url中域名所命中的finalRules规则,找出第一个能用的规则。
  2. getDomainReplaceRuleByRequestUrl。该函数主要用于非 XMLHttpRequest拦截(例如window.open等),该函数将从finalRules命中的域名的规则中,找出仅域名替换的规则并返回。

如上2个函数的具体实现如下(src/rulesManager/index.js):

js

// 根据请求参数,找出该url命中的最佳规则. 最佳规则结构:{type: 'tunnel' | 'xhr', targetUrl: string}
export 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
    }
}

export function getDomainReplaceRuleByRequestUrl(reqOptions) {
    return getRuleByRequestUrl(reqOptions, true)
}