如何为HTML页面所有HTTP请求前置拼接代理URL
页面全量HTTP请求前置拼接代理URL实现方案
由于Service Worker需要等待页面加载完成后才能注册生效,无法覆盖页面初始化阶段发出的请求,可选择以下两类方案实现加载前即生效的请求代理拼接:
方案1:页面头部阻塞脚本提前劫持原生请求API
- 核心逻辑:在HTML文档
<head>标签的最开头插入一段无async/defer属性的同步阻塞脚本,在所有其他资源、业务脚本加载执行前,覆写浏览器所有发起网络请求的原生入口,所有请求发出前自动拼接代理前缀。 - 需要覆盖的请求入口:
- 覆写
window.fetch,拦截所有fetch发起的接口、资源请求 - 覆写
XMLHttpRequest.prototype.open,拦截所有XHR异步请求 - 通过
Object.defineProperty劫持各类资源标签的src/href属性赋值逻辑,覆盖img、script、link、iframe、音视频等标签的静态资源加载请求
- 覆写
- 参考实现代码:
<!-- 必须放在head最顶部,所有其他meta、资源、脚本之前,保证最先执行 --> <script> // 替换为实际使用的代理地址前缀 const PROXY_URL_PREFIX = 'https://your-proxy-address/forward?target='; // 过滤不需要走代理的特殊协议 const SKIP_PROTOCOL_PREFIX = ['data:', 'blob:', 'about:']; // 拦截fetch请求 const rawFetch = window.fetch; window.fetch = function(requestInit, config) { let targetUrl = typeof requestInit === 'string' ? requestInit : requestInit.url; const needProxy = !targetUrl.startsWith(PROXY_URL_PREFIX) && !SKIP_PROTOCOL_PREFIX.some(prefix => targetUrl.startsWith(prefix)); if (needProxy) { targetUrl = PROXY_URL_PREFIX + encodeURIComponent(targetUrl); requestInit = typeof requestInit === 'string' ? targetUrl : new Request(targetUrl, requestInit); } return rawFetch.call(this, requestInit, config); }; // 拦截XHR请求 const rawXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, ...restArgs) { const needProxy = !url.startsWith(PROXY_URL_PREFIX) && !SKIP_PROTOCOL_PREFIX.some(prefix => url.startsWith(prefix)); if (needProxy) { url = PROXY_URL_PREFIX + encodeURIComponent(url); } return rawXHROpen.call(this, method, url, ...restArgs); }; // 拦截标签静态资源请求 const resourceHookList = [ [HTMLImageElement.prototype, 'src'], [HTMLScriptElement.prototype, 'src'], [HTMLLinkElement.prototype, 'href'], [HTMLIFrameElement.prototype, 'src'], [HTMLAudioElement.prototype, 'src'], [HTMLVideoElement.prototype, 'src'], [HTMLFormElement.prototype, 'action'] ]; resourceHookList.forEach(([elementProto, property]) => { const originalPropDescriptor = Object.getOwnPropertyDescriptor(elementProto, property); Object.defineProperty(elementProto, property, { get() { return originalPropDescriptor.get.call(this); }, set(newValue) { const needProxy = newValue && !newValue.startsWith(PROXY_URL_PREFIX) && !SKIP_PROTOCOL_PREFIX.some(prefix => newValue.startsWith(prefix)); const finalValue = needProxy ? PROXY_URL_PREFIX + encodeURIComponent(newValue) : newValue; return originalPropDescriptor.set.call(this, finalValue); }, configurable: true }); }); </script>
- 注意事项:这段脚本不能加任何异步加载属性,必须保证在文档解析的第一时间执行,否则会漏掉脚本执行前已经发起的请求。
方案2:网络层/服务端前置处理,零前端侵入
如果不想在前端写劫持逻辑,或者需要100%覆盖所有请求无遗漏,可以选择更上层的处理方案:
- 服务端处理:托管页面的服务端在返回HTML文档时,直接把文档里所有的资源、接口URL提前替换为拼接好代理前缀的地址,不需要前端运行时做任何处理,页面加载时自然所有请求都走代理地址。
- 全局网络代理:直接给浏览器、运行容器配置正向代理,所有网络请求从系统/浏览器网络层直接转发到代理服务,完全不依赖页面内的逻辑,不存在任何时机问题。
避坑提示:不要在DOMContentLoaded、load等页面事件触发后再做请求劫持,这时候页面初始化阶段的请求已经全部发出,和Service Worker的时机问题一致,无法达到提前生效的要求。
内容的提问来源于stack exchange,提问作者Eli Ozcan
相关产品推荐
相关产品推荐

