AWS
高级
变通方案

解决方案:为 public/ 目录下的每个顶级文件和文件夹创建单独的缓存行为(AWS 特定)

资源文件部分所述,应用 public/ 文件夹中的文件是静态资源,会被上传到 S3 存储桶。对这些文件的请求将由 S3 存储桶直接处理,例如:

https://my-nextjs-app.com/favicon.ico
https://my-nextjs-app.com/my-images/avatar.png

理想情况下,我们会创建一个缓存行为来将所有 public/ 文件的请求路由到 S3 存储桶。但遗憾的是,CloudFront 不支持在缓存行为中使用正则表达式或高级字符串模式(例如 /favicon.ico|my-images\/*/)。

为解决这一限制,我们需要为 public/ 目录下的每个顶级文件和文件夹分别创建缓存行为。例如,如果你的目录结构如下:

public/
  favicon.ico
  my-images/
    avatar.png
    avatar-dark.png
  foo/
    bar.png

则需要创建三个缓存行为:/favicon.ico/my-images/*/foo/*。每个行为都指向 S3 存储桶。

需要注意的是,CloudFront 默认每个分发最多只能有 25 个缓存行为 (opens in a new tab)。如果你有大量顶级文件和文件夹,可能会达到此限制。为避免这种情况,可以考虑将部分或全部文件和文件夹移至子目录中:

public/
  files/
    favicon.ico
    my-images/
      avatar.png
      avatar-dark.png
    foo/
      bar.png

这样只需要创建一个缓存行为:/files/*

请确保相应地更新代码以反映新的文件路径。

另外,你也可以通过 AWS 支持服务申请提高限制 (opens in a new tab)

解决方案:设置 x-forwarded-host 请求头(AWS 特定场景)

当服务器函数接收请求时,Lambda 请求头中的 host 值会被设置为 AWS Lambda 服务的域名,而非实际前端域名。这会导致服务器函数(中间件、SSR 路由或 API 路由)在需要获取前端域名时出现问题。

为解决此问题,可以在 Viewer Request 阶段运行一个 CloudFront 函数,将前端域名设置为 x-forwarded-host 请求头。函数代码如下:

function handler(event) {
  var request = event.request;
  request.headers["x-forwarded-host"] = request.headers.host;
  return request;
}

之后,服务器函数在向 NextServer 发送请求时,会将请求的 host 请求头设置为 x-forwarded-host 请求头的值。

解决方案:设置 NextRequest 的地理位置数据

当您的应用托管在 Vercel 上时,可以通过 NextRequest 对象在中间件中访问用户的地理位置信息。

export function middleware(request: NextRequest) {
  request.geo.country;
  request.geo.city;
}

当应用托管在 AWS 上时,您可以从 CloudFront 请求头中获取地理位置数据 (opens in a new tab)。但默认情况下无法将这些数据设置到传递给中间件函数的 NextRequest 对象上。

为解决此问题,我们修改了 NextRequest 构造函数,使其从 CloudFront 头信息初始化地理位置数据,而不是使用默认的空对象。

- geo: init.geo || {}
+ geo: init.geo || {
+   country: this.headers("cloudfront-viewer-country"),
+   countryName: this.headers("cloudfront-viewer-country-name"),
+   region: this.headers("cloudfront-viewer-country-region"),
+   regionName: this.headers("cloudfront-viewer-country-region-name"),
+   city: this.headers("cloudfront-viewer-city"),
+   postalCode: this.headers("cloudfront-viewer-postal-code"),
+   timeZone: this.headers("cloudfront-viewer-time-zone"),
+   latitude: this.headers("cloudfront-viewer-latitude"),
+   longitude: this.headers("cloudfront-viewer-longitude"),
+   metroCode: this.headers("cloudfront-viewer-metro-code"),
+ }

CloudFront 提供了更详细的地理位置信息,如邮政编码和时区。以下是中间件中可用的完整 geo 属性列表:

export function middleware(request: NextRequest) {
  // Next.js 原生支持
  request.geo.country;
  request.geo.region;
  request.geo.city;
  request.geo.latitude;
  request.geo.longitude;
 
  // OpenNext 额外支持
  request.geo.countryName;
  request.geo.regionName;
  request.geo.postalCode;
  request.geo.timeZone;
  request.geo.metroCode;
}

解决方案:NextServer 未为 HTML 页面设置缓存头

服务端函数章节所述,服务端函数使用 Next.js 构建输出中的 NextServer 类来处理请求。然而,NextServer 似乎没有设置正确的 Cache Control 头。

为解决此问题,服务端函数会检查请求是否针对页面路由器的完全静态 HTML 页面(即不包含 getStaticProps),并设置 Cache Control 头为:

public, max-age=0, s-maxage=31536000, must-revalidate

如果您计划使用完全静态的 HTML 页面,还应将 x-middleware-prefetch 头添加到 CloudFront 的缓存头中,以避免当该头被设置时 CloudFront 缓存空响应。 您也可以直接在页面中添加一个空的 getStaticProps 函数,这将设置正确的缓存头。

解决方案:NextServer 未设置正确的 SWR 缓存头

NextServer 似乎没有为 stale-while-revalidate 缓存头设置适当的值。例如,该头可能如下所示:

s-maxage=600 stale-while-revalidate

这会阻止 CloudFront 缓存陈旧数据。

为解决此问题,服务端函数会检查响应是否包含 stale-while-revalidate 头。如果找到,则将其值设为 30 天:

s-maxage=600 stale-while-revalidate=2592000

解决方案:设置 NextServer 工作目录(AWS 特定)

Next.js 推荐使用 process.cwd() 而非 __dirname 来获取应用目录。例如,假设你的应用中有一个包含 markdown 文件的 posts 文件夹:

pages/
posts/
  my-post.md
public/
next.config.js
package.json

你可以这样构建文件路径:

path.join(process.cwd(), "posts", "my-post.md");

服务端函数部分所述,在非 monorepo 项目中,server-function 的打包结构如下:

.next/
node_modules/
posts/
  my-post.md    <- 路径为 "posts/my-post.md"
index.mjs

这种情况下,path.join(process.cwd(), "posts", "my-post.md") 能够解析到正确的路径。

然而,当用户的应用位于 monorepo 中(例如在 /packages/web 目录下),server-function 的打包结构会变成:

packages/
  web/
    .next/
    node_modules/
    posts/
      my-post.md    <- 路径为 "packages/web/posts/my-post.md"
    index.mjs
node_modules/
index.mjs

此时,path.join(process.cwd(), "posts", "my-post.md") 将无法正确解析。

为了解决这个问题,我们将服务端函数的工作目录修改为 .next/ 所在的路径,即 packages/web

临时解决方案:设置 __NEXT_PRIVATE_PREBUNDLED_REACT 使用预打包的 React

对于 Next.js 13.2 及更高版本,你需要显式设置 __NEXT_PRIVATE_PREBUNDLED_REACT 环境变量。虽然目前该环境变量尚未在文档中说明,但你可以参考 Next.js 源代码来理解其用途:

在独立模式下,我们没有分离的渲染工作线程,因此如果同时使用了 app 和 pages 路由,我们需要解析到预打包的 React 以确保 app 路由使用正确的版本。

使用静态路径引入这些模块,以确保在独立模式下构建应用时 NFT 能够跟踪它们,因为我们现在有条件地对它们进行别名处理,在构建时跟踪这些模块变得比较复杂。

每次请求时,我们会尝试检测路由是使用 Pages Router 还是 App Router。如果使用的是 Pages Router,我们会将 __NEXT_PRIVATE_PREBUNDLED_REACT 设置为 undefined,这意味着会使用 node_modules 中的 React 版本。然而,如果使用的是 App Router,则会设置 __NEXT_PRIVATE_PREBUNDLED_REACT,并使用预打包的 React 版本。

临时解决方案:13.4.13+ 版本的重大变更(中间件、重定向、重写)

Next.js 13.4.13 重构了中间件逻辑,使其不再在服务器处理程序中运行。相反,它们作为工作线程在子线程中执行,这会引入约 5 秒的不可接受的延迟。为了解决这个问题,open-next 需要在处理服务器处理程序之前自行实现中间件处理程序。

我们引入了一个自定义的 esbuild 插件,用于有条件地注入和覆盖代码,以正确处理这些重大变更。

默认的请求处理程序位于 adapters/plugins/default.ts 当 open-next 由于 Next.js 的兼容性破坏需要覆盖该实现时,build.ts 中的 createServerBundle 会确定适当的覆盖项来替换 default.ts 文件的代码。