解决方案:为 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
文件的代码。