页面依赖 CDN 上的一段脚本,地址加载失败时,可以尝试备用地址。但“失败就换一个”还不够,至少要明确失败的类型、重试次数,以及是否可能重复执行。
下面只处理经典 script 的资源加载错误:候选地址按顺序尝试,成功就停止,全部失败就提示。
两个地址,一个故意失败
创建 index.html:
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8" /><meta name="viewport" content="width=device-width" />
<title>脚本备用地址</title
><button id="load">加载</button>
<pre id="result"></pre>
<script type="module">
async function loadScript(urls) {
for (const url of urls) {
const loaded = await new Promise((resolve) => {
const script = document.createElement("script");
script.src = url;
script.onload = () => resolve(true);
script.onerror = () => {
script.remove();
resolve(false);
};
document.head.append(script);
});
if (loaded) return;
}
throw new Error("所有脚本地址都加载失败");
}
window.loadScript = loadScript;
document.querySelector("#load").addEventListener("click", async (event) => {
event.currentTarget.disabled = true;
try {
await loadScript(["/missing.js", "/working.js"]);
document.querySelector("#result").textContent =
"执行次数 " + window.fallbackExecuted;
} catch (error) {
document.querySelector("#result").textContent = error.message;
}
});
</script>
</html>
创建 server.mjs:
import http from "node:http";
import { readFile } from "node:fs/promises";
http
.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname === "/working.js") {
res.setHeader("Content-Type", "text/javascript");
return res.end("window.fallbackExecuted=(window.fallbackExecuted||0)+1;");
}
if (url.pathname !== "/") {
res.writeHead(404);
return res.end();
}
res.setHeader("Content-Type", "text/html; charset=utf-8");
try {
res.end(await readFile(new URL("index.html", import.meta.url)));
} catch {
res.writeHead(500);
res.end("缺少 index.html");
}
})
.listen(3000, "127.0.0.1");
node server.mjs 运行后访问 http://127.0.0.1:3000,点击加载。missing.js 返回 404,working.js 成功,页面显示执行次数 1。按钮禁用,避免这个演示重复加载成功脚本。
在控制台执行 await loadScript([‘/missing-a.js’,‘/missing-b.js’]),两个地址都失败,应得到明确的错误。之前成功脚本的计数不会因为这次失败增加。
函数的候选列表有限,但如果请求一直挂住,仍可能一直等待。它没有实现超时切换,不能称为完整的 CDN 容灾方案。
资源错误和业务错误分开判断
script 的 onload 不保证业务初始化成功。脚本抛运行时错误,也不能直接认定下载失败再执行一个副本:第一份代码可能已经注册事件或修改了全局状态。
同样,定时器到期就切换地址,旧请求后续可能仍会执行。需要超时方案时,还得设计执行唯一性,而不是只增加一个 Promise.race。
这段函数适合独立经典脚本。模块脚本、带依赖顺序的资源和样式要按各自的加载契约处理。事件说明参考 MDN script 文档。
备用资源也要进入发布流程
两个 CDN 上的同名文件可能不是同一版本。可以使用内容哈希命名、同步发布并检查备用资源实际存在。CSP 要允许候选来源,使用 SRI 时内容也要一致,跨源配置要匹配。
这个本地例子确认了 404 后尝试下一项,以及全部失败时结束,没有模拟真实区域故障。接入构建之前,先明确运行时契约,再考虑从构建产物生成备用地址列表。