script.onerror 不能直接赋值 this.src 实现降级,因会导致死循环、Safari 不触发、200 状态码返回 HTML 错误页却无法捕获等三类问题;安全做法是新建 script 元素、加防重入标记、分离加载与执行。

script 标签加载失败时,浏览器不会自动重试或切换资源——onerror 是唯一能捕获失败的原生钩子,但它的行为在不同场景下差异极大,不加区分地写 this.src = 'fallback.js' 很可能触发无限循环或静默失效。
为什么 script.onerror 不能只靠赋值回退
直接在 onerror 里改 this.src 看似简单,但实际有三类典型崩坏:
- 如果 fallback 脚本也加载失败,
onerror会再次触发,形成死循环(尤其当两个 URL 指向同一不可达域名时) - 动态插入的
script元素,其onerror在某些 Safari 版本中根本不会触发(已知 iOS 15.4–16.2 存在该 bug) - CDN 返回 404 但 HTTP 状态码是 200(比如 Nginx 自定义错误页),此时
onerror不触发,脚本内容却是 HTML 错误页,执行时报SyntaxError——这压根捕获不到
怎么安全地实现 JS 加载降级
核心思路:分离“加载”和“执行”,失败后不复用原 script 元素,而是新建一个、并设防重入标记。
- 给每个
script添加唯一data-id,并在window上记录已尝试过的 ID,避免重复 fallback - fallback 脚本加载前,先检查
document.readyState;若为loading,延迟到DOMContentLoaded后再插入,防止 DOM 未就绪导致执行异常 - 对 fallback 脚本也绑定
onerror,但只触发一次警告日志,不再降级——三级回退没有意义,只会掩盖真实问题 - 务必在 fallback 脚本内部做最小可用性验证,比如暴露一个全局函数
window.fallbackReady,主逻辑等待它存在再继续
示例片段:
立即学习“前端免费学习笔记(深入)”;
const loadScriptWithFallback = (mainUrl, fallbackUrl) => {
const id = `script-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
const script = document.createElement('script');
script.src = mainUrl;
script.dataset.id = id;
const handleError = () => {
if (window._triedFallbacks?.has(id)) return;
window._triedFallbacks = window._triedFallbacks || new Set();
window._triedFallbacks.add(id);
const fb = document.createElement('script');
fb.src = fallbackUrl;
fb.onload = () => window.dispatchEvent(new Event('fallback-loaded'));
fb.onerror = () => console.warn(`Fallback ${fallbackUrl} also failed`);
document.head.appendChild(fb);
};
script.onerror = handleError;
document.head.appendChild(script);
};
内联脚本执行失败怎么兜底
内联 <script>...</script> 没有 onerror,语法错误或运行时报错会直接中断后续脚本,且无法拦截。能做的只有预防性隔离:
- 把关键逻辑包进
try/catch,并在catch中调用降级函数(如显示提示、启用简化 UI) - 避免在内联脚本中操作尚未存在的 DOM 节点;用
document.currentScript定位自身位置,再向上找容器,比硬写getElementById更健壮 - 不要在内联脚本里写
fetch或setTimeout等异步逻辑——一旦外层脚本挂掉,这些回调永远不会执行,降级逻辑就彻底失联 - 如果必须动态加载,优先用
import()动态导入(配合try/catch),它比eval或字符串拼接更可控,且现代打包工具能自动切分 chunk
CDN 脚本挂了,用户连按钮都点不了怎么办
真正的底线不是换一个 JS 文件,而是让核心交互不依赖远程脚本。这意味着:
- 表单提交、搜索框回车、导航链接这些基础动作,必须用纯 HTML 属性完成(
form action、a href),JS 只负责增强,不负责实现 - 所有通过 JS 绑定的事件,都要在对应 HTML 元素上预留
data-fallback-action属性,比如data-fallback-action="submit",降级时用click触发原生行为 - 如果项目用了框架(React/Vue),必须保留 SSR 或静态生成的 HTML 副本,确保 JS 加载失败时页面仍有可点击结构——这点常被忽略,但恰恰是用户能否完成任务的最后一道防线
最危险的不是脚本加载失败,而是开发者默认“它总会成功”,然后把整个交互链路建在一层薄薄的网络请求之上。



















