我们发布了一个基于 Astro 构建的双语(法语/阿拉伯语)市场网站——静态输出、数百个位置和服务页面,以及几千个专业个人资料页面,每个页面都有一张从 Firebase Storage 获取的照片。没什么特别的。然后有一天,生产构建突然……挂了。本地开发没有警告,没有失败的测试,也没有 lint 错误。npm run build 以非零状态退出,堆栈跟踪指向 astro:assets。
深入调查后发现的原因:其中一张个人资料照片的 URL 已失效。Firebase Storage 的下载令牌已轮换,因此原本能返回 JPEG 的请求现在返回 403。就是这样。一个失效的 URL,在几千个中,导致了整个构建崩溃。
这让我感到意外,因为我原本以为 Astro 的图片管道会对坏的远程图片进行软失败——记录警告、跳过它、继续处理。它并没有这样做。以下是原因,以及我们采取的措施。
Astro 的 组件和底层的 getImage() 函数都移交给 Image Service,其工作是 transform() 图片——调整大小、转换格式,无论你的配置是什么。这是你可以介入和自定义的部分。
问题是 transform() 只接收已经获取的图片,作为原始缓冲区。对远程 src 的实际网络请求在更早的 Astro 内部 loadRemoteImage 步骤中发生——这不是公共的可插拔 Image Service API 的一部分。没有受支持的钩子位于“这里有一个远程 URL”和“这里是获取的缓冲区”之间,你可以拦截失败的请求并决定如何处理。
因此,从框架的角度来看,远程图片的 403 或超时不是“优雅处理”的情况——而是静态生成期间的致命错误。如果你的数据集中甚至有一个图片 URL 失效,整个构建就会停止。
两分钟的修复(以及为什么不够)
最快的出路很明显:将 替换为普通的 标签。这完全绕过了 Astro 的构建时获取和转换步骤,因为普通的 只是按原样发出 URL,让浏览器在运行时处理。
这立即解除了我们的部署阻塞,但这是一个真正的权衡,而不是修复——你失去了自动调整大小、格式转换(WebP/AVIF)和响应式 srcset 处理,而这正是使用 的全部原因。作为权宜之计可以。不是可以长期保留的东西。
实际的修复:在构建前检查,而不是在构建期间
真正的问题不在于 Astro 处理失败的方式不好——而在于我们将未经验证的 URL 交给了 Astro。所以我们将验证移到了更早的阶段,移到了在 Astro 接触图片之前运行的 prebuild 步骤。
脚本很小:收集你的页面将引用的每个唯一远程图片 URL,使用超时对每个进行 HEAD 检查,并将失败的写入 JSON 清单。
js
// scripts/validate-image-urls.mjs
import fs from "node:fs";
const TIMEOUT_MS = 8000;
const CONCURRENCY = 10;
async function checkUrl(url) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(url, { method: "HEAD", signal: controller.signal });
return res.ok;
} catch {
return false; // 超时、DNS 故障、连接重置——都视为“坏”
} finally {
clearTimeout(timer);
}
}
比代码本身更重要的两个设计决策:
按 URL 失败关闭,按聚合失败打开。如果单个 URL 超时或出错,将其视为坏——这是单个图片的安全默认值。但是,如果在同一次运行中,比如 70% 的 URL 都失败,那不是“我们的大部分图片在一夜之间失效了”,而是几乎可以肯定检查器本身出问题了——离线的 CI 运行器、DNS 故障、速率限制。在这种情况下,脚本会保留现有的清单,而不是清空网站上的所有照片。验证步骤中的部分网络故障不应能够破坏你的生产内容。
通过 prebuild 而不是 Astro 集成来挂接它。我们通过 npm 的生命周期(package.json 中的“prebuild”: “node scripts/validate-image-urls.mjs”)连接它,它会在构建前自动运行。我们故意跳过了将其编写为 Astro 集成钩子——它更简单,完全与 Astro 的内部构建顺序解耦,并且在框架迁移时不受影响。
在组件方面,只需一行检查:
astro
import { isBadImageUrl } from "../utils/badImageUrls";
const showFallback = isBadImageUrl(profile.thumbnail);
{showFallback
? /* 渲染首字母头像代替 / null
: / 使用 profile.thumbnail 作为源渲染优化的 Image 组件 */ null}
已知坏的 URL 会获得一个优雅的回退(首字母头像),而不是到达 。其他所有内容都正常获得完整的优化管道。
它在规模上真的有效吗?
第一次真正的生产运行在几秒钟内检查了数千个唯一的图片 URL 与实时网络的连接,这要归功于并发限制,并且它完全按照预期工作:标记了少数真正失效的 URL,保留了其他所有内容,并且构建不再依赖于互联网上每个图片主机的正常运行时间。
在发布此内容时取得成效的一个小习惯:当我将紧急 回退恢复为 后,我对比了 git 历史记录(git show HEAD:path/to/file),而不是相信我对原始 props 的记忆。廉价的保险——任何时候在时间压力下撤销紧急修复时都值得这样做。
要点
如果你使用 Astro 的 或 getImage() 处理你不完全控制的远程源——用户上传、CMS、云存储——不要假设坏的 URL 会优雅地失败。它不会,这是设计使然,因为获取发生在你无法挂接的管道部分。在构建开始前验证,而不是在构建期间。
我们在 bricoleapp.com 上运行此功能,这是一个面向摩洛哥和埃及的双语家政服务市场——如果有用,很乐意深入探讨任何部分。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.