HTTPS 页面加载 HTTP 脚本被拦?同源代理来救场

4 阅读5分钟

前言:跨域与 HTTPS→HTTP 场景下的 UMD 加载

做「配置化加载第三方 UMD 模块」这类页面时,经常需要根据配置动态加载其他域名或 HTTP 地址上的 UMD 脚本,再在页面里挂载对应组件。听起来就是:发个请求、插个 <script src="...">、等 onload、从 window[globalName] 取到 init 调一下——很简单对吧?

现实是:HTTPS 页面请求 HTTP 资源(混合内容)、跨域、Referrer 策略、script.onload 玄学全撞一起,请求明明 200 了页面却一直转圈,控制台还报混合内容被拦截。本文聚焦跨域HTTPS 发往 HTTP 这两类场景的踩坑与解法,如果你也在做「页面里动态拉 UMD 并初始化」的活儿,希望可以少走弯路。

踩坑经历

坑一:Referrer 策略导致 HTTPS→HTTP 跨域请求被拒

现象:从 https://your-site.com 去请求另一域名的 HTTP 资源(如 http://other-cdn.com/...)时,请求失败或异常,控制台/网络里能看到和「引荐来源政策:strict-origin-when-cross-origin」相关的内容。

原因:浏览器默认的 Referrer 策略是 strict-origin-when-cross-origin。从 HTTPS 发往 HTTP 的跨域请求里,Referrer 会被不发送或降级,防止来源泄露。对端若校验 Referrer、或者对「没有 Referrer」的请求处理不当,就会直接拒掉或返回异常。

解决:统一「不带 Referrer」发请求,避免策略差异。

  • fetch(例如用来 detectContentType 时):加上 referrerPolicy: "no-referrer"credentials: "omit"
  • 动态插入的 <script>:设置 script.referrerPolicy = "no-referrer",加载 UMD 时不带 Referrer。

另外,跨域或 HTTP 的 URL 在「先 fetch 探测内容类型」时可能因为各种原因返回失败。这时不要直接当错误拦掉,可以加一个判断:若 URL 是跨域或协议为 http:(即属于 HTTPS→HTTP 场景),即使 detectContentType 报错,也**继续走「插 script + 轮询全局对象」**的流程,用最终是否拿到 window[globalName] 来判定成败。

坑二:请求 200 却一直「加载中」——别只信 script.onload

现象:UMD 脚本的请求已经 200,但页面一直显示「加载中」,永远进不到内容展示。

原因主要有两点:

  1. script.onload 不一定触发:跨域脚本在某些环境下(MIME 类型、执行报错、跨域策略等),即使返回 200,onload 也可能不触发,导致你依赖 onload 的「检测全局对象并调用 init」逻辑永远不执行。
  2. 全局名对不上:UMD 实际挂到 window 上的名字(例如 window.MyRobot)和配置里的 packageName(GlobalName)不一致,轮询一直在找 window[globalName].init,当然找不到。

解决:不把「加载完成」押宝在单一 onload 上,用轮询 + 超时 + 明确报错兜底。

  • 轮询:插入 <script> 后启动定时器(例如每 300ms)检查 window[globalName]?.init,一旦存在就调用 init、结束 loading、清理所有定时器。
  • 统一初始化逻辑:抽出 tryInitFromGlobal(),在 onload 回调和轮询里都调用,谁先成功谁收尾,避免重复执行。
  • 超时与报错
    • 20 秒内若仍没有检测到全局对象,清除轮询并提示:未检测到全局对象 "xxx",请确认 UMD 导出的全局名称与模块配置的 packageName 一致
    • 30 秒整体加载超时(脚本一直没返回):清除 script 和所有定时器,报「加载超时」。
  • 清理:用 timeoutIdRefpollIntervalRef 保存定时器 ID,在成功、失败或组件卸载时统一 clearTimeout / clearInterval,避免内存泄漏。
// 轮询 + onload 双保险示例
function tryInitFromGlobal() {
  const g = window[globalName]
  if (g?.init) {
    g.init(containerRef.current, props)
    setLoading(false)
    clearLoadTimers()
    return true
  }
  return false
}

// 插入 script 后
script.onload = () => {
  if (!tryInitFromGlobal()) {
    pollIntervalRef.current = setInterval(() => {
      if (tryInitFromGlobal()) clearInterval(pollIntervalRef.current)
    }, 300)
  }
}
// 同时启动 20s「未检测到全局对象」、30s「加载超时」的定时器,并在卸载时 clearLoadTimers()

坑三:HTTPS 页面加载 HTTP 脚本被拦(混合内容)

现象:控制台报错:The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...'. This request has been blocked

原因:浏览器禁止在 HTTPS 页面里直接加载 HTTP 资源(脚本、样式、iframe 等),这是安全策略,前端无法关闭。若 UMD 脚本只有 HTTP 地址可用,就必须通过「同源的 HTTPS 地址」间接拿到脚本内容。

解决思路:同源代理。前端不直接请求 HTTP 的脚本地址,而是请求自家同源接口,例如:

GET https://your-domain.com/api/umd-proxy?url=encodeURIComponent(http://other-cdn.com/xxx.js)

后端(或开发环境下 Vite 的中间件)收到后,在服务端去请求该 HTTP URL,把响应体(和合适的 Content-Type)原样返回;前端用这个同源 URL 作为 script.src,或先 fetch 再 Blob/内联执行。这样对浏览器而言全是 HTTPS 同源,不会触发混合内容。

实现要点

  • 前端:封装 getUmdLoadUrl(umdUrl)——若当前页是 https:umdUrlhttp:,则返回
    ${location.origin}/api/umd-proxy?url=${encodeURIComponent(umdUrl)},否则返回原 umdUrl。内容类型检测和 script.src 都用这个「最终加载 URL」。
  • 开发环境(Vite):在 vite.config.ts 里加一个 umdProxyMiddleware,用 enforce: 'pre' 保证在别的 /api 代理之前处理 /api/umd-proxy。从 query 读 url只允许白名单内的 HTTP 地址,服务端 fetch 该 URL 后把响应写回,并设置 Content-Type: application/javascript
  • 生产环境:由后端提供 GET /api/umd-proxy?url=...,做 URL 白名单校验后再请求目标 URL 并回写。前端逻辑不用区分环境,统一用 getUmdLoadUrl() 即可。

安全:代理必须做 URL 白名单(仅允许可信域名),避免被滥用成开放代理。

小结

UMD 动态加载在跨域HTTPS 发往 HTTP 场景下,容易遇到三类问题:

  1. Referrer 策略:HTTPS→HTTP 跨域请求被拒时,给 fetch 和动态 script 都加上 referrerPolicy: "no-referrer";跨域或 HTTP URL 探测失败时不要一棒子打死,可以继续走插 script + 轮询。
  2. script.onload 不可靠:用轮询检测 window[globalName]?.init 做兜底,并做 20s/30s 超时与明确错误提示;保证 UMD 的全局名与配置的 packageName 一致。
  3. 混合内容:HTTPS 页面不能直接加载 HTTP 脚本,用同源代理(/api/umd-proxy?url=...)在服务端拉取 HTTP 再返回,代理端务必做 URL 白名单。

如果你也在做「页面里动态拉 UMD」的活儿,希望这篇能帮你少踩几个坑。