How to self-host your Next.js application
当部署你的 Next.js 应用程序时,你可能希望根据基础设施配置不同功能的处理方式。
🎥 观看: 了解更多关于自托管 Next.js 的内容 → YouTube(45 分钟)。
Image Optimization
通过 next/image 进行的 Image Optimization 在使用 next start 部署时可以零配置自托管。如果你希望使用单独的服务来优化图片,可以配置图片加载器。
Image Optimization 可以与静态导出一起使用,方法是在 next.config.js 中定义自定义图片加载器。请注意,图片是在运行时优化的,而不是在构建期间。
值得注意的是:
- 在基于 glibc 的 Linux 系统上,Image Optimization 可能需要额外配置以防止过度内存使用。
- 了解更多关于优化图片的缓存行为以及如何配置 TTL。
- 你也可以禁用 Image Optimization,同时仍然保留使用
next/image的其他好处。例如,如果你单独优化图片。
Proxy
Proxy 在使用 next start 部署时可以零配置自托管。由于它需要访问传入请求,因此在使用静态导出时不受支持。
Proxy 使用 Edge runtime,这是所有可用 Node.js API 的一个子集,以帮助确保低延迟,因为它可能在应用程序中的每个路由或资源前运行。如果你不希望这样,可以使用完整的 Node.js runtime 来运行 Proxy。
如果你希望添加需要所有 Node.js API 的逻辑(或使用外部包),你可能可以将此逻辑移至布局作为服务器组件。例如,检查 headers 和重定向。你还可以使用 headers、cookies 或查询参数通过 next.config.js 进行重定向或重写。如果这不起作用,你也可以使用自定义服务器。
Environment Variables
Next.js 可以同时支持构建时和运行时环境变量。
默认情况下,环境变量仅在服务器上可用。要将环境变量暴露给浏览器,必须以 NEXT_PUBLIC_ 为前缀。但是,这些公共环境变量将在 next build 期间内联到 JavaScript bundle 中。
要读取运行时环境变量,我们建议使用 getServerSideProps 或逐步采用 App Router。
这允许你使用单个 Docker 镜像,该镜像可以在具有不同值的多个环境中提升。
值得注意的是:
- 你可以使用
register函数在服务器启动时运行代码。
Caching 和 ISR
Next.js 可以缓存响应、生成的静态页面、构建输出以及其他静态资源,如图片、字体和脚本。
缓存和重新验证页面(使用 Incremental Static Regeneration)使用相同的共享缓存。默认情况下,此缓存存储在 Next.js 服务器的文件系统(磁盘上)。使用 Pages 和 App Router 进行自托管时,这会自动工作。
如果你想将缓存的页面和数据持久化到持久存储,或在 Next.js 应用程序的多个容器或实例之间共享缓存,可以配置 Next.js 缓存位置。
自动缓存
- Next.js 为真正不可变的资源设置
Cache-Control标头为public, max-age=31536000, immutable。它无法被覆盖。这些不可变文件在文件名中包含 SHA 哈希,因此可以安全地无限期缓存。例如,静态图片导入。你可以为图片配置 TTL。 - Incremental Static Regeneration (ISR) 设置
Cache-Control标头为s-maxage: <revalidate in getStaticProps>, stale-while-revalidate。此重新验证时间在你的getStaticProps函数中以秒为单位定义。如果你设置revalidate: false,它将默认为一年的缓存持续时间。 - 动态渲染的页面设置
Cache-Control标头为private, no-cache, no-store, max-age=0, must-revalidate,以防止缓存用户特定的数据。这适用于 App Router 和 Pages Router。这也包括 Draft Mode。
静态资源
如果你想在不同的域或 CDN 上托管静态资源,可以在 next.config.js 中使用 assetPrefix 配置。Next.js 在检索 JavaScript 或 CSS 文件时将使用此资源前缀。将资源分离到不同的域确实会带来在 DNS 和 TLS 解析上花费额外时间的缺点。
配置缓存
默认情况下,生成的缓存资源将存储在内存中(默认为 50mb)和磁盘上。如果你使用容器编排平台(如 Kubernetes)托管 Next.js,每个 pod 将拥有缓存的副本。为了防止显示过时数据(因为默认情况下缓存不在 pod 之间共享),你可以配置 Next.js 缓存以提供缓存处理程序并禁用内存缓存。
要在自托管时配置 ISR/Data Cache 位置,可以在 next.config.js 文件中配置自定义处理程序:
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // 禁用默认的内存缓存
}然后,在项目的根目录中创建 cache-handler.js,例如:
const cache = new Map()
module.exports = class CacheHandler {
constructor(options) {
this.options = options
}
async get(key) {
// 这可以存储在任何地方,比如持久存储
return cache.get(key)
}
async set(key, data, ctx) {
// 这可以存储在任何地方,比如持久存储
cache.set(key, {
value: data,
lastModified: Date.now(),
tags: ctx.tags,
})
}
async revalidateTag(tags) {
// tags 是字符串或字符串数组
tags = [tags].flat()
// 遍历缓存中的所有条目
for (let [key, value] of cache) {
// 如果值的标签包含指定的标签,则删除此条目
if (value.tags.some((tag) => tags.includes(tag))) {
cache.delete(key)
}
}
}
// 如果你想为单个请求设置临时内存缓存,该缓存在下一个请求之前重置
// 你可以利用此方法
resetRequestCache() {}
}使用自定义缓存处理程序可以确保托管 Next.js 应用程序的所有 pod 之间的一致性。例如,你可以将缓存值保存在任何地方,如 Redis 或 AWS S3。
值得注意的是:
revalidatePath是缓存标签之上的便利层。调用revalidatePath将使用为提供的页面提供的特殊默认标签调用revalidateTag函数。
Build Cache
Next.js 在 next build 期间生成一个 ID,以标识正在提供哪个版本的应用程序。同一个构建应该被使用并启动多个容器。
如果你为环境的每个阶段重新构建,则需要生成一个一致的构建 ID 以在容器之间使用。在 next.config.js 中使用 generateBuildId 命令:
module.exports = {
generateBuildId: async () => {
// 这可以是任何东西,使用最新的 git 哈希
return process.env.GIT_HASH
},
}Version Skew
Next.js 将自动缓解大多数版本偏差实例,并在检测到时自动重新加载应用程序以检索新资源。例如,如果 deploymentId 不匹配,页面之间的转换将执行硬导航,而不是使用预取值。
当应用程序重新加载时,如果应用程序状态未设计为在页面导航之间持久化,则可能会丢失应用程序状态。例如,使用 URL 状态或本地存储将在页面刷新后持久化状态。但是,像 useState 这样的组件状态将在此类导航中丢失。
手动优雅关闭
自托管时,你可能希望在服务器因 SIGTERM 或 SIGINT 信号关闭时运行代码。
你可以将环境变量 NEXT_MANUAL_SIG_HANDLE 设置为 true,然后在 _document.js 文件中为该信号注册处理程序。你需要直接在 package.json 脚本中注册环境变量,而不是在 .env 文件中。
值得注意的是: 手动信号处理在
next dev中不可用。
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "NEXT_MANUAL_SIG_HANDLE=true next start"
}
}if (process.env.NEXT_MANUAL_SIG_HANDLE) {
process.on('SIGTERM', () => {
console.log('Received SIGTERM: cleaning up')
process.exit(0)
})
process.on('SIGINT', () => {
console.log('Received SIGINT: cleaning up')
process.exit(0)
})
}