在 Next.js 中用 getImageProps 优化 CMS 图片
当你渲染来自 WordPress 这类 CMS 的内容时,图片可能是以光秃秃的 <img src="..." alt="..."> 标签形式到达的:没有 srcset、没有懒加载、没有 WebP。Next.js 没法自动处理它们,因为它们藏在一个原始的 HTML 字符串里,而不是 React 组件树里。这篇文章会展示如何用 Next.js 的 getImageProps,在每张图片到达浏览器之前,就为它注入恰当的 srcset、WebP 转换、懒加载和响应式尺寸。
为什么 CMS 图片会破坏 Next.js 的优化
Next.js 图片优化是使用这个框架的最佳理由之一。自动 WebP 转换、懒加载、恰当的 srcset 生成,这些它都能搞定。但前提是你得用 next/image 里的 <Image> 组件。
当你从 WordPress 或任何 headless CMS 拉取一篇博客文章的正文时,你得到的是一个原始的 HTML 字符串。那个字符串里包含的是普通的 <img> 标签,除了一个 src 和一个 alt 之外什么都没有。这些图片会以完整分辨率加载,阻塞渲染,并拖垮你的 Core Web Vitals 分数。
你有两个选择:在渲染时把每张图片解析成一个 React <Image> 组件,或者在 HTML 字符串到达客户端之前,先在服务端对它做预处理。服务端的做法更干净:它把关注点分离开来,让你的渲染组件保持简单。
getImageProps 究竟做了什么
getImageProps 是 next/image 导出的一个更底层的 API。它暴露了与驱动 <Image> 组件相同的优化逻辑,只不过是以一个普通的异步函数调用形式出现,返回的是 props 而不是 JSX。
你传给它一个 src、一个 alt、一个 sizes,以及可选的 width 和 height。它会返回一个 props 对象,里面有一个优化过的 src(指向 Next.js 的图片 CDN)、一个完整的 srcSet,以及 loading 和 decoding 的合理默认值。你可以把这些 props 直接展开(spread)到任意 <img> 元素上。
这正是你需要的。你不是去渲染组件,而是处理一个字符串:找到那些 <img> 标签,跑一遍 getImageProps,再把优化过的属性写回到 HTML 字符串里。
optimizeImages 工具函数:它是如何工作的
这个工具函数由四个各司其职的小函数构成。在你把实现代码贴进来之前,先看看每个部分都做了什么。
parseImgAttributes 接收一个原始的 <img> 标签字符串,返回一个由它的属性组成的普通键值对象。它会剥掉开标签的语法,然后在剩下的文本上跑一个正则,捕获属性名和属性值。键会被转成小写,所以无论 CMS 是怎么序列化这段标记的,attrs.src 始终都靠得住。
buildImgTag 做的是相反的事:它接收那个键值映射,把它重新序列化成一个有效的 <img /> 字符串。它会对属性值里的 & 和 " 进行转义,以保证 HTML 有效。
optimizeImageTag 是核心。它用解析出来的 src 和 alt 调用 getImageProps,然后把返回的 props 合并回原始的属性映射里。如果 getImageProps 因为任何原因抛出错误(某个没在 next.config 里列出的域名、一个格式有误的 URL),catch 块就会给原始标签加上懒加载和异步解码,然后继续往下走。
CONTENT_IMAGE_SIZES 这个常量告诉浏览器,图片在移动端会占满整个视口,在更宽的屏幕上则封顶在 768px。这对应的是典型的博客正文列。请把它调整成与你实际布局断点相匹配。
最后,optimizeImages 把这一切串了起来。它用一个正则找出 HTML 字符串里所有的 <img> 标签,通过 Promise.all 在每一个上面并行跑 optimizeImageTag,然后按顺序替换掉原始标签。
更进一步:获取真实的图片尺寸
你可能注意到了原始代码里的 // width, height 注释。当 getImageProps 知道源图片的固有尺寸时,它生成的 srcset 条目会更准确。没有这些尺寸,Next.js 就会退回到一组通用的候选宽度,而那组宽度可能跟你的图片完全对不上。
提供尺寸还能防止布局偏移。当浏览器在图片加载之前就知道宽高比,它就能在文档流中预留出正确的空间。这会直接改善你的 累积布局偏移(CLS)分数。
probe-image-size 这个包只会从一个图片 URL 里读取刚好够提取尺寸的那一点字节,它并不会下载整个文件。先安装它:
npm install probe-image-size
npm install -save-dev @types/probe-image-size然后把这个函数加到你的工具文件里:
用 html-react-parser 安全地渲染 HTML
一旦 optimizeImages 处理完你的 HTML 字符串,你就需要在 React 里把它渲染出来。大多数开发者首先会去抓的那个选项是 dangerouslySetInnerHTML。这个名字不只是一句警告:它精确地描述了它在做什么。
直接设置 innerHTML 会完全绕过 React。 React 没法对那些 DOM 节点做协调(reconcile),没法给它们里面的元素挂上事件处理器,而且一旦 CMS 内容是用户生成的或被攻破的,它也没法阻止跨站脚本(XSS)。你还会失去 hydration 的正确性:服务端和客户端可能产生不同的 DOM,从而引发一些隐蔽的 bug。
正确的工具是 html-react-parser。它会把 HTML 字符串解析成一棵 React 元素树,而不是原始的 DOM 节点。React 拥有这份输出,hydration 能正确工作,而且你还可以用 replace 选项拦截任意元素,在需要时把它换成一个自定义组件。
| 做法 | XSS 安全 | React 协调 | 事件处理器 | Hydration |
|---|---|---|---|---|
dangerouslySetInnerHTML | 否 | 否 | 否 | 不可靠 |
html-react-parser | 是 | 是 | 是 | 正确 |
replace 回调还能当作一张安全网。万一有哪个 <img> 标签溜过了服务端的优化步骤,你可以在这里把它逮住,转而渲染一个 Next.js 的 <Image> 组件。它给了你一个干净的切入点进入解析树,而无需你自己去写一个 HTML 遍历器。
把它们全都串起来
整条流水线很直接。从 CMS 取回你文章的正文,在服务端把 HTML 字符串过一遍 optimizeImages,再把结果交给你的组件,用 html-react-parser 渲染出来。你的读者拿到的是尺寸恰当、懒加载的 WebP 格式图片,而你既没碰过 CMS,也没改过任何一个模板。
这个做法的可扩展性也很好。由于 optimizeImages 是在构建时或在 Server Component 里运行的,所以没有任何客户端开销。所有重活都发生在 HTML 到达浏览器之前。