banner
innei

innei

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

一次构建多处部署 - Next.js Runtime Env

我们一般通过控制 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


加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。