banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

一次構建多處部署 - Next.js 運行時環境

我們一般通過控制 env 的方式去做到 "Build once, deploy many" 哲學。但是在 Next.js 中,環境變量分為兩種,一種是可被用於 Client 側的 NEXT_PUBLIC_ 開頭的環境變量,另一種是只能被用於 Server 側的環境變量。前者會在 Next.js 構建時被注入到客戶端代碼中,導致原有代碼被替換,那麼也就意味著我們控制 env 並不能做到一次構建多處部署。一旦需要部署到不同的環境並且修改 env,我們就需要重新構建一次。

image

今天的文章,我們將會探討如何通過 Next.js 的 Runtime Env 來實現一次構建多處部署。

Next.js Runtime Env#

今天的主角是 next-runtime-env 這個庫,它可以讓我們在 Next.js 中使用 Runtime Env。我們可以通過它來實現一次構建多處部署。

npm i next-runtime-env

更換 Client 側的環境變量使用方式:

import { env } from 'next-runtime-env'

const API_URL = process.env.NEXT_PUBLIC_API_URL
const API_URL = env('NEXT_PUBLIC_API_URL') 

export const fetchJson = () => fetch(API_URL as string).then((r) => r.json())

然後在 app/layout.tsx 上增加環境變量注入 Script。

import { PublicEnvScript } from 'next-runtime-env'

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <head>
        <PublicEnvScript />
      </head>
      <body className={inter.className}>{children}</body>
    </html>
  )
}

那麼這樣就可以了。

現在我們來試一試。我們有這樣頁面,直接渲染上述 API_URL 的響應數據。

'use client'

export default function Home() {
  const [json, setJson] = useState(null)
  useEffect(() => {
    fetchJson().then((r) => setJson(r))
  }, [])
  return JSON.stringify(json)
}

現在我們使用 next build 構建項目,然後在構建之後,修改 .env 中的 NEXT_PUBLIC_API_URL,然後使用 next start 啟動項目,觀察實際請求的接口是否隨著 .env 的修改而變化。

現在我們的 NEXT_PUBLIC_API_URL=https://jsonplaceholder.typicode.com/todos/2,啟動項目之後,瀏覽器請求的是 https://jsonplaceholder.typicode.com/todos/2

image

當我們修改 .env 中的 NEXT_PUBLIC_API_URLhttps://jsonplaceholder.typicode.com/todos/3,然後重啟項目,瀏覽器請求的是 https://jsonplaceholder.typicode.com/todos/3

image

這樣我們就實現了一次構建多處部署,只需要修改 env 即可。

深入了解 Runtime Env#

其實 next-runtime-env 的實現原理非常簡單,<PublicEnvScript /> 實際就是在 <head> 中注入了一個 <script /> 類似這樣。

<script data-testid="env-script">window['__ENV'] = {"NEXT_PUBLIC_API_URL":"https://jsonplaceholder.typicode.com/todos/3"}</script>

由於 <head /> 中的 script 會在頁面水合前被執行,所以我們可以在 Client 側通過 window['__ENV'] 來獲取環境變量,而 next-runtime-env 提供 env()正是這樣實現的。而這個環境變量在 Server Side 都是動態的,所以在 Server Side 的取值永遠都是通過 process.env[']

下面的簡略的代碼展示了 env() 的實現。

export function env(key: string): string | undefined {
  if (isBrowser()) {
    if (!key.startsWith('NEXT_PUBLIC_')) {
      throw new Error(
        `Environment variable '${key}' is not public and cannot be accessed in the browser.`,
      );
    }

    return window['__ENV'][key];
  }

  return process.env[key];
}

構建一個無環境變量依賴的產物#

一個項目中,一般都會存在大量的環境變量,有部分環境變量只會在 Client Side 使用,在項目 build 過程中,必須要正確的注入環境變量,否則會導致項目無法通過構建。

例如常見的 API_URL 變量,是請求接口的地址,在構建中,如果沒有值,就會導致預渲染中的接口請求錯誤導致構建失敗。比如在 Route Handler 中,我們有這樣一個函數。

import { NextResponse } from 'next/server'

import { fetchJson } from '../../../lib/api'

export const GET = async () => {
  await fetchJson()
  return NextResponse.json({})
}

API_URL 為空時,fetchJson 會報錯,導致構建失敗。

 ✓ Collecting page data    
   Generating static pages (0/6)  [    ]
Error occurred prerendering page "/feed". Read more: https://nextjs.org/docs/messages/prerender-error

TypeError: Failed to parse URL from 

這是因為在 Next.js 中,默認對 Route handler 進行了預渲染,而在預渲染過程中,fetchJson 會被執行,而 API_URL 為空,導致請求失敗。

只需要使用 noStore() 或者改變 dynamic 的方式,就可以解決這個問題。

import { unstable_noStore } from 'next/cache'
import { NextResponse } from 'next/server'

import { fetchJson } from '../../../lib/api'

export const dynamic = 'force-dynamic' // 方式 2

export const GET = async () => {
  unstable_noStore() // 方式 1
  await fetchJson()
  return NextResponse.json({})
}

那麼,在其他的頁面構建中,如果也遇到類似的問題,也修改這個地方就可以了。

構建的時候,我們沒有注入任何的環境變量,在啟動構建後的服務之前,記得一定要在當前目錄下創建一個 .env 文件,並且正確填寫變量值,這樣才能保證項目正常運行。

通過 Dockerfile 構建無環境變量依賴的鏡像#

在上節的基礎上,對整個構建過程進一步封裝,使用 Docker 完成整個構建然後發布到 Docker Hub,真正意義上實現一次構建多處部署。

創建一個 Dockerfile 文件。

FROM node:18-alpine AS base

RUN npm install -g --arch=x64 --platform=linux sharp

FROM base AS deps

RUN apk add --no-cache libc6-compat
RUN apk add --no-cache python3 make g++

WORKDIR /app

COPY . .

RUN npm install -g pnpm
RUN pnpm install

FROM base AS builder

RUN apk update && apk add --no-cache git


WORKDIR /app
COPY --from=deps /app/ .
RUN npm install -g pnpm

ENV NODE_ENV production
RUN pnpm build

FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

# and other docker env inject
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./.next/server

EXPOSE 2323

ENV PORT 2323
ENV NEXT_SHARP_PATH=/usr/local/lib/node_modules/sharp
CMD node server.js;

上面的 dockerfile 在官網版本的基礎上做了修改,已在 Shiro 中落地使用。

由於 Next.js standalone build 中並不包含 sharp 依賴,所以在 Docker 構建中我們首先全局安裝了 sharp,並且在後續注入了 sharp 的安裝位置的環境變量。

這樣構建的 Docker 鏡像也不依賴於環境變量,並且 standalone build 讓 Docker image 的佔用空間更小。

通過 Docker 容器的路徑映射,我們只需要把當前目錄下的 .env 映射到容器內部的 /app/.env 即可。

這裡編寫一個簡單的 Docker compose 實例。

version: '3'

services:
  shiro:
    container_name: shiro
    image: innei/shiro:latest
    volumes:
      - ./.env:/app/.env # 映射 .env 文件
    restart: always
    ports:
      - 2323:2323

大功告成,後續任何人只需要通過 Docker pull 取得構建後的鏡像然後再修改本地 .env 就能夠運行屬於自己環境的項目了。

此文由 Mix Space 同步更新至 xLog 原始鏈接為 https://innei.in/posts/tech/nextjs-runtime-env-and-build-once-deploy-many

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。