前言:跨域与 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,但页面一直显示「加载中」,永远进不到内容展示。
原因主要有两点:
script.onload不一定触发:跨域脚本在某些环境下(MIME 类型、执行报错、跨域策略等),即使返回 200,onload也可能不触发,导致你依赖onload的「检测全局对象并调用 init」逻辑永远不执行。- 全局名对不上: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 和所有定时器,报「加载超时」。
- 20 秒内若仍没有检测到全局对象,清除轮询并提示:
- 清理:用
timeoutIdRef、pollIntervalRef保存定时器 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:且umdUrl是http:,则返回
${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 场景下,容易遇到三类问题:
- Referrer 策略:HTTPS→HTTP 跨域请求被拒时,给 fetch 和动态 script 都加上
referrerPolicy: "no-referrer";跨域或 HTTP URL 探测失败时不要一棒子打死,可以继续走插 script + 轮询。 - script.onload 不可靠:用轮询检测
window[globalName]?.init做兜底,并做 20s/30s 超时与明确错误提示;保证 UMD 的全局名与配置的 packageName 一致。 - 混合内容:HTTPS 页面不能直接加载 HTTP 脚本,用同源代理(
/api/umd-proxy?url=...)在服务端拉取 HTTP 再返回,代理端务必做 URL 白名单。
如果你也在做「页面里动态拉 UMD」的活儿,希望这篇能帮你少踩几个坑。