如何自託管你的 Next.js 應用程式

部署您的 Next.js 應用程式時,您可能需要根據基礎架構配置不同功能的處理方式。

🎥 觀看影片: 深入了解自行託管 Next.js → YouTube (45 分鐘)

圖片最佳化

透過 next/image 進行的圖片最佳化在使用 next start 部署時,無需配置即可自行託管。如果您希望使用單獨的服務來最佳化圖片,可以配置圖片載入器

圖片最佳化可與靜態匯出一起使用,方法是在 next.config.js 中定義自訂圖片載入器。請注意,圖片是在執行時最佳化,而非建置期間。

須知事項:

  • 在基於 glibc 的 Linux 系統上,圖片最佳化可能需要額外配置以防止過度記憶體使用。
  • 深入了解最佳化圖片的快取行為以及如何配置 TTL。
  • 如果您願意,也可以停用圖片最佳化,同時保留使用 next/image 的其他好處。例如,如果您自行單獨最佳化圖片。

中介軟體

中介軟體在使用 next start 部署時,無需配置即可自行託管。由於它需要存取傳入請求,因此在使用靜態匯出時不受支援。

中介軟體使用邊緣運行時,這是所有可用 Node.js API 的子集,以幫助確保低延遲,因為它可能在應用程式的每個路由或資產前運行。如果您不希望這樣,可以使用完整的 Node.js 運行時來運行中介軟體。

如果您希望新增需要所有 Node.js API 的邏輯(或使用外部套件),您可能可以將此邏輯移至佈局作為伺服器元件。例如,檢查標頭重新導向。您也可以使用標頭、Cookie 或查詢參數透過 next.config.js 進行重新導向重寫。如果這不起作用,您也可以使用自訂伺服器

環境變數

Next.js 支援建置時和執行時環境變數。

預設情況下,環境變數僅在伺服器上可用。要將環境變數公開給瀏覽器,必須加上 NEXT_PUBLIC_ 前綴。然而,這些公開環境變數將在 next build 期間內聯到 JavaScript 套件中。

要讀取執行時環境變數,我們建議使用 getServerSideProps逐步採用應用程式路由器

這允許您使用單一的 Docker 映像,可以在多個環境中推廣,並使用不同的值。

須知事項:

  • 您可以使用 register 函式在伺服器啟動時執行程式碼。
  • 我們不建議使用 runtimeConfig 選項,因為這不適用於獨立輸出模式。相反,我們建議逐步採用應用程式路由器。

快取與 ISR

Next.js 可以快取回應、生成的靜態頁面、建置輸出以及其他靜態資產,如圖片、字型和腳本。

快取和重新驗證頁面(使用增量靜態再生)使用相同的共享快取。預設情況下,此快取儲存在 Next.js 伺服器的檔案系統(磁碟)上。這在使用頁面和應用程式路由器自行託管時自動運作

如果您希望將快取的頁面和資料持久化到持久儲存,或在 Next.js 應用程式的多個容器或實例之間共享快取,可以配置 Next.js 快取位置。

自動快取

  • Next.js 將 Cache-Control 標頭設定為 public, max-age=31536000, immutable 給真正不可變的資產。這無法覆蓋。這些不可變檔案在檔案名稱中包含 SHA 雜湊,因此可以安全地永久快取。例如,靜態圖片匯入。您可以配置 TTL 給圖片。
  • 增量靜態再生 (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 以防止使用者特定資料被快取。這適用於應用程式路由器和頁面路由器。這也包括草稿模式

靜態資產

如果您希望在不同的網域或 CDN 上託管靜態資產,可以在 next.config.js 中使用 assetPrefix 配置。Next.js 將在檢索 JavaScript 或 CSS 檔案時使用此資產前綴。將資產分離到不同的網域會帶來 DNS 和 TLS 解析的額外時間。

深入了解 assetPrefix

配置快取

預設情況下,生成的快取資產將儲存在記憶體中(預設為 50mb)和磁碟上。如果您使用 Kubernetes 等容器編排平台託管 Next.js,每個 pod 將擁有快取的副本。為了防止顯示過時資料,因為預設情況下快取不在 pod 之間共享,您可以配置 Next.js 快取以提供快取處理程式並停用記憶體快取。

在自行託管時配置 ISR/資料快取位置,您可以在 next.config.js 檔案中配置自訂處理程式:

next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // 停用預設的記憶體快取
}

然後,在專案的根目錄中建立 cache-handler.js,例如:

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 函式。

建置快取

Next.js 在 next build 期間生成一個 ID,以識別正在提供的應用程式版本。相同的建置應在多個容器中使用和啟動。

如果您為環境的每個階段重新建置,則需要生成一致的建置 ID 以在容器之間使用。在 next.config.js 中使用 generateBuildId 命令:

next.config.js
module.exports = {
  generateBuildId: async () => {
    // 這可以是任何內容,使用最新的 git 雜湊
    return process.env.GIT_HASH
  },
}

版本偏差

Next.js 將自動緩解大多數版本偏差的實例,並在檢測到時自動重新載入應用程式以檢索新資產。例如,如果 deploymentId 不匹配,頁面之間的轉換將執行硬導航而非使用預取值。

當應用程式重新載入時,如果未設計為在頁面導航之間持久化,則可能會丟失應用程式狀態。例如,使用 URL 狀態或本地儲存將在頁面重新整理後持久化狀態。然而,元件狀態如 useState 將在此類導航中丟失。

手動優雅關機

在自行託管時,您可能希望在伺服器關閉時在 SIGTERMSIGINT 信號上執行程式碼。

您可以將環境變數 NEXT_MANUAL_SIG_HANDLE 設定為 true,然後在 _document.js 檔案中為該信號註冊處理程式。您需要直接在 package.json 腳本中註冊環境變數,而不是在 .env 檔案中。

須知事項:手動信號處理在 next dev 中不可用。

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "NEXT_MANUAL_SIG_HANDLE=true next start"
  }
}
pages/_document.js
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)
  })
}

On this page