整体流程
图解
流程描述:
- 首先每个页面项目中,会通过npm方式安装fetBlock依赖库,并在页面逻辑较早位置调用 fetBlock.init()方法。开启fetBlock。即图中左侧第一个框。
- 当fetBlock.init桩代码执行时,会做如下2件事情(即上图第二列内容):
- 通过原生XHR api去拉取远程的fetBlockLogic.js逻辑代码并存储到localstorage中。fetBlockLogic.js才是我们真正的页面api hook逻辑。
- 如果之前已经拉取过了fetBlockLogic.js,则会优先用本地localstorage的先执行,以尽快进行页面内hook拦截。
- 上图第三列的内容:当第二步骤中fetBlockLogic.js逻辑js执行过程中,其内部逻辑会通过浏览器原生XHR拉取远程的fetBlockConfig.json,以获得该用户的拦截配置。同时会对所有页面内api进行hook。
- 当fetBlockLogic.js拦截到了页面内的某个请求,会判断该请求是否命中fetBlockConfig.json中配置的拦截规则。若命中,则根据规则进行拦截。(例如xhr请求可能会根据规则走到客户端通道)。
封禁对抗逻辑fetBlockLogis.js为什么不打包到产物
之所以将fet-block-logic.js逻辑放到远程每次进行拉取更新,是因为业务中实际页面项目可能有几百个(尤其对于活动页类型的公司场景),因此为了能让改动立刻全网生效,避免每个项目都重新构建打包,我们必须把逻辑放到远程。并由各个项目中恒久不变的stub桩代码来拉取执行。
如果打包到产物,一旦我们封禁对抗logic逻辑部分有改动,则要发布所有的项目,成本巨大。
性能的权衡
第一,封禁对抗不应该阻塞主逻辑,起码在远程还未拉到对抗逻辑之前咱们不能阻塞主渲染流程。第二,封禁对抗库要尽可能早的执行,才能让页面内各种资源尽可能早的完成替换,否则会漏掉一些较早发出的请求。
为了解决如上2点,我们通过把资源异步更新放置到localstorage,以实现“更新是异步”,“执行是同步”。唯一的缺点是,假如用户是首次进入页面,则其本地没有缓存,需要先拉取一次。但在该场景下,这种case是可接受的。
另外一种解决方案是:将封禁逻辑fetBlockLogic.js和fetBlockConfig.json都由服务端直出 html 的时候,直接注入到html里面(例如以script src标签的形式注入)。这样我们就可以将js和json文件名加上哈希,这样每次更新时,只要html文件名变了,浏览器就会重新请求;而如果文件名没变则一般浏览器都会有上次的缓存。这是在有服务端Node.js的场景下比较推荐的策略。而有的公司暂时没有nodejs服务端,则只能选择上述localstorage方案。
如何做到localstorage代码同步执行
前端js中localtorage的读取天然是同步行为,当我们拿到其中的js代码后,要想线程内同步立刻执行,则可以通过 document.head.appendChild这样的script插入方式,这种内部包含script内容的标签插入dom后的那一刻被浏览器立即执行。
有的同学会会问:为什么非要同步执行? 答案是:因为我们封禁对抗fetBlockLogic.js逻辑对执行时机有着非常高的要求,一旦执行晚了,那你的一些页面内资源/接口请求都已经发出去了。此时我们fetBlockLogic再hook就已经晚了。 因此必须确保要优先让 fetBlockLogic 能在页面其他逻辑运行之前先执行。
如何避免logic.js缓存
由于cdn上的js资源通常都有较长时间的缓存头,因此你异步拉取远程logic.js的时候有可能根本不会发出请求。同时,即使发出请求,cdn边缘节点也可能存在缓存。
为了解决此问题,我们需要:
- 本地请求时在url末尾增加随机数。例如?t=Date.now()
- 在cdn缓存边缘节点中,设置当url的query变化时,缓存也失效。即要按url的path+query来缓存。关于这一点,还有个解决方案是,每次你发布fetBlockLogic.js和fetBlockConfig.json之后,主动调用一下cdn厂商的边缘节点缓存刷新接口,从而让cdn边缘节点更新。当然了,还有一种方案时采用 logic.js.html这样的扩展名或其他短缓存的扩展名,由于一般我们html的缓存头设置为60秒或不缓存,基于此可以避免手机端或cdn节点的缓存。