next.js的Image在请求网络图片的过程中做了什么?

1,436 阅读2分钟

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 }
   }
}

总结

  1. next.js 在处理客户端代码时会将 src 参数转换为 /_next/image?url=xxx&w=xxx&q=xxx 型的字符串
  2. 当这个请求发送到 next.js 服务端的时候,会对其进行缓存管理。