一、Next.js 环境变量机制速览
Next.js 内置对 .env 文件的支持,但其行为有关键安全边界:
| 变量前缀 | 是否暴露给客户端 | 构建时内联 | 运行时可变 |
|---|---|---|---|
NEXT_PUBLIC_ |
✅ 是 | ✅ 是 | ❌ 否(构建后冻结) |
无前缀(如 DB_URL) |
❌ 否 | ❌ 否 | ✅ 是(仅服务端) |
⚠️ 重要原则 :
永远不要将密钥、数据库地址、内部 API 以NEXT_PUBLIC_开头!
二、企业级多环境目录结构
python
your-nextjs-app/
├── .env.local # ← 本地开发(gitignore)
├── .env.development # ← 开发环境默认值(可提交)
├── .env.test # ← 测试环境默认值(可提交)
├── .env.production # ← 生产环境默认值(可提交)
├── src/
│ ├── lib/
│ │ └── env.ts # ← 统一加载 & 类型校验
│ └── app/
└── next.config.js
✅ Git 提交策略:
.env.*(无.local后缀)→ 提交到仓库(含安全默认值).env.local→ 加入.gitignore(含个人/敏感密钥)
三、分环境配置实战代码
1. 定义各环境 .env 文件
.env.development(开发默认)
env
# 公共配置(客户端可用)
NEXT_PUBLIC_APP_NAME=MyApp Dev
NEXT_PUBLIC_API_BASE=/api
# 服务端配置(仅服务端)
DB_URL=postgresql://dev:dev@localhost:5432/myapp_dev
AUTH_SECRET=dev-secret-change-in-prod
LOG_LEVEL=debug
.env.test(测试环境)
env
NEXT_PUBLIC_APP_NAME=MyApp Test
NEXT_PUBLIC_API_BASE=https://api.test.myapp.com
DB_URL=postgresql://test:test@test-db:5432/myapp_test
AUTH_SECRET=test-secret-12345
LOG_LEVEL=info
.env.production(生产环境)
env
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_API_BASE=https://api.myapp.com
# 注意:DB_URL 和 AUTH_SECRET 实际由 CI/CD 注入,此处仅为类型占位
DB_URL=***OVERRIDE_IN_DEPLOYMENT***
AUTH_SECRET=***OVERRIDE_IN_DEPLOYMENT***
LOG_LEVEL=warn
💡 提示 :生产环境的敏感值通常由部署平台(Vercel/Docker/K8s)注入,
.env.production仅用于类型提示和本地模拟。
2. 统一加载与类型校验(src/lib/env.ts)
ts
// src/lib/env.ts
import { z } from 'zod'
// 定义环境变量 Schema
const EnvSchema = z.object({
// 客户端变量(NEXT_PUBLIC_)
NEXT_PUBLIC_APP_NAME: z.string(),
NEXT_PUBLIC_API_BASE: z.string().url(),
// 服务端变量
DB_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),
})
// 加载并校验
let env: z.infer<typeof EnvSchema>
try {
env = EnvSchema.parse(process.env)
} catch (error) {
console.error('❌ Invalid environment variables:', error)
process.exit(1)
}
export { env }
✅ 优势:
- 启动时校验缺失/格式错误
- 提供完整 TypeScript 类型提示
- 避免散落在各处的
process.env.XXX
3. 在 App Router 中使用
服务端组件(安全访问所有变量)
tsx
// src/app/page.tsx
import { env } from '@/lib/env'
import { sql } from '@/lib/db'
export default async function HomePage() {
// ✅ 安全:DB_URL 仅在服务端可用
const [userCount] = await sql`SELECT COUNT(*) FROM users`
return (
<div>
<h1>{env.NEXT_PUBLIC_APP_NAME}</h1>
<p>User count: {userCount.count}</p>
</div>
)
}
客户端组件(仅能访问 NEXT_PUBLIC_)
tsx
// src/app/ui/Header.tsx
'use client'
import { env } from '@/lib/env' // ← 会报错!env 含服务端变量
// ✅ 正确做法:只导入公共变量
const { NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_API_BASE } = process.env
export default function Header() {
return <header>{NEXT_PUBLIC_APP_NAME}</header>
}
🔒 安全加固 :在
src/lib/env.ts中拆分客户端/服务端对象:
tsexport const publicEnv = { NEXT_PUBLIC_APP_NAME: env.NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_API_BASE: env.NEXT_PUBLIC_API_BASE, }
四、测试环境特殊处理
Next.js 不会自动加载 .env.test,需手动指定:
方案 1:运行测试时显式设置 NODE_ENV=test
bash
# package.json
{
"scripts": {
"test": "NODE_ENV=test jest",
"test:e2e": "NODE_ENV=test playwright test"
}
}
方案 2:在测试 setup 中加载(推荐)
ts
// test/setup-env.ts
import { loadEnvConfig } from '@next/env'
// 加载 .env.test
loadEnvConfig(process.cwd(), false, { info: () => {}, error: console.error })
然后在 Jest/Playwright 配置中引入:
js
// jest.config.js
module.exports = {
setupFiles: ['./test/setup-env.ts'],
}
✅ 效果:测试时
process.env自动包含.env.test内容。
五、Docker 多阶段部署示例
dockerfile
# Dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY . .
# 构建时注入公共变量(注意:非敏感!)
ARG NEXT_PUBLIC_APP_NAME
ARG NEXT_PUBLIC_API_BASE
ENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME
ENV NEXT_PUBLIC_API_BASE=$NEXT_PUBLIC_API_BASE
RUN npm run build
FROM base AS runner
# 运行时注入敏感变量(通过 docker run -e)
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
部署命令:
bash
# 构建(仅传公共变量)
docker build \
--build-arg NEXT_PUBLIC_APP_NAME="MyApp" \
--build-arg NEXT_PUBLIC_API_BASE="https://api.myapp.com" \
-t myapp .
# 运行(传敏感变量)
docker run -d \
-e DB_URL="postgresql://prod:xxx@prod-db/..." \
-e AUTH_SECRET="very-long-secret-here" \
-p 3000:3000 \
myapp
✅ 安全分离:构建镜像不含密钥,密钥仅在运行时注入。
六、常见陷阱与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
process.env.DB_URL 在客户端为 undefined |
未加 NEXT_PUBLIC_ |
正确:服务端变量绝不暴露给客户端 |
| 生产环境变量未更新 | NEXT_PUBLIC_ 在构建时冻结 |
敏感配置走 API 获取或服务端注入 |
| 测试时连错数据库 | 未加载 .env.test |
使用 loadEnvConfig 或设 NODE_ENV=test |
.env.local 被误提交 |
未加 .gitignore |
立即加入:echo ".env.local" >> .gitignore |
结语:环境变量是信任边界
在 Next.js 的混合渲染模型中,环境变量不仅是配置,更是安全边界的体现。通过分层设计(公共/私有)、类型校验、多环境隔离,我们既能享受开发便利,又能守住安全底线。