IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    Innei发表于 2024-03-29 12:27:33
    love 0
    该渲染由 Shiro API 生成,可能存在排版问题,最佳体验请前往:https://innei.in/posts/tech/nextjs-runtime-env-and-build-once-deploy-many

    我们一般通过控制 env 的方式去做到 "Build once, deploy many" 哲学。但是在 Next.js 中,环境变量分为两种,一个种是可被用于 Client 侧的 NEXT_PUBLIC_ 开头的环境变量,另一个种是只能被用于 Server 侧的环境变量。前者会在 Next.js 构建时被注入到客户端代码中,导致原有代码被替换,那么也就意味着我们控制 env 并不能做到一次构建多处部署。一旦需要部署到不同的环境并且修改 env,我们就需要重新构建一次。

    今天的文章,我们将会探讨如何通过 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 // [!code --]
    const API_URL = env('NEXT_PUBLIC_API_URL') // [!code ++]
    
    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 /> // [!code ++]
          </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。

    当我们修改 .env 中的 NEXT_PUBLIC_API_URL 为 https://jsonplaceholder.typicode.com/todos/3,然后重启项目,浏览器请求的是 https://jsonplaceholder.typicode.com/todos/3。

    这样我们就实现了一次构建多处部署,只需要修改 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 就能够运行属于自己环境的项目了。

    看完了?说点什么呢



沪ICP备19023445号-2号
友情链接