next.js 建议我们使用 next/image 渲染图片:
import Image from "next/image";
const Item = () => {
return (
<Image
src={image}
alt={""}
width={240}
height={320}
/>
)
}
Next.js 在这个组件内部对图片进行了缓存处理,接下来我们通过阅读源码对这个过程进行解读。
处理src
在 next.js/image.tsx 中,可以发现我们给 next/Image 传入的 src 默认会使用 defaultLoader 方法做了处理。
function defaultLoader({
root,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
...
return `${root}?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}`
}
它返回一个经过处理的 url 地址,它的样子类似于:/_next/image?url=xxx&w=xxx&q=xxx。
服务端下载远程图片并进行缓存
当 Server 服务器 next.js/next-server… 匹配到这个模式的请求时,会对其进行处理:
protected generateImageRoutes(): Route[] {
return [
{
match: route('/_next/image'),
type: 'route',
name: '_next/image catchall',
fn: (req, res, _params, parsedUrl) => {
if (this.minimalMode) {
res.statusCode = 400
res.body('Bad Request').send()
return {
finished: true,
}
}
return this.imageOptimizer(
req as NodeNextRequest,
res as NodeNextResponse,
parsedUrl
)
},
},
]
}
它会调用相同文件下 this.imageOptimizer 这个图片优化方法:
protected async imageOptimizer(
req: NodeNextRequest,
res: NodeNextResponse,
parsedUrl: UrlWithParsedQuery
): Promise<{ finished: boolean }> {
const { imageOptimizer } =
require('./image-optimizer') as typeof import('./image-optimizer')
return imageOptimizer(
req.originalRequest,
res.originalResponse,
parsedUrl,
this.nextConfig,
this.distDir,
() => this.render404(req, res, parsedUrl),
(newReq, newRes, newParsedUrl) =>
this.getRequestHandler()(
new NodeNextRequest(newReq),
new NodeNextResponse(newRes),
newParsedUrl
),
this.renderOpts.dev
)
}
而这个方法会找到另一个文件 image-optimizer 中的同名方法 ,这个方法比较长,有五百行,主要做了两件事情:
- 如果服务器本地有缓存,取本地的缓存数据
- 如果服务器本地没有缓存,则请求获取远程数据并写入缓存
简化的 imageOptimizer :
export async function imageOptimizer(){
// 如果有缓存,取缓存
const imagesDir = join(distDir, 'cache', 'images')
const hashDir = join(imagesDir, hash)
if (await fileExists(hashDir, 'directory')) {
const files = await promises.readdir(hashDir)
for (let file of files) {
// ...
const isFresh = now < expireAt
const result = setResponseHeaders(...)
if (!result.finished) { // 如果文件没有传输完成
createReadStream(fsPath).pipe(res) // 将文件写入响应数据流
}
if (isFresh) { // 如果过期了
return { finished: true }
} else {
await promises.unlink(fsPath) // 删除服务器文件
staleWhileRevalidate = true // swr,告知浏览器在新数据响应之前使用已过期的缓存数据
}
}
}
// 如果没有缓存,则判断是网络图片还是本地图片
if (isAbsolute) {
const upstreamRes = await fetch(href) // 用fetch获取图片
upstreamType =
detectContentType(upstreamBuffer) ||
upstreamRes.headers.get('Content-Type')
...
}else{
// 请求本地图片
const mockRes: any = new Stream.Writable()
mockRes.write = (chunk: Buffer | string) => {
resBuffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
upstreamType =
detectContentType(upstreamBuffer) || mockRes.getHeader('Content-Type')
...
}
// 写入缓存
if (upstreamType) {
await writeToCacheDir(
hashDir,
upstreamType,
maxAge,
expireAt,
upstreamBuffer
)
sendResponse(
req,
res,
url,
maxAge,
upstreamType,
upstreamBuffer,
isStatic,
isDev,
staleWhileRevalidate
)
return { finished: true }
}
}
总结
- next.js 在处理客户端代码时会将
src参数转换为/_next/image?url=xxx&w=xxx&q=xxx型的字符串 - 当这个请求发送到 next.js 服务端的时候,会对其进行缓存管理。