Skip to content

环境变量与配置

🙋 API 密钥、数据库连接串这些敏感信息怎么管理?如何区分开发和生产环境?

环境变量基础#

.env 文件#

.env # 所有环境,提交到 git
.env.local # 本地覆盖,不提交
.env.development # 开发环境
.env.production # 生产环境
.env.test # 测试环境

加载优先级(高到低):

.env.development.local
.env.local
.env.development
.env

定义环境变量#

.env.local
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
API_SECRET="super-secret-key"
# 客户端可访问(需要 NEXT_PUBLIC_ 前缀)
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_GA_ID="G-XXXXXXXXXX"

使用环境变量#

// 服务端(任意环境变量)
const dbUrl = process.env.DATABASE_URL
const secret = process.env.API_SECRET
// 客户端(仅 NEXT_PUBLIC_ 前缀)
const appUrl = process.env.NEXT_PUBLIC_APP_URL
// ❌ 客户端无法访问
const secret = process.env.API_SECRET // undefined

类型安全#

环境变量验证#

lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
// 服务端变量
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional(),
// 客户端变量
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
})
// 验证并导出
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
console.error('❌ Invalid environment variables:', parsed.error.format())
throw new Error('Invalid environment variables')
}
export const env = parsed.data

使用类型安全的环境变量#

app/page.tsx
import { env } from '@/lib/env'
export default function Page() {
// 类型安全,IDE 自动补全
const appUrl = env.NEXT_PUBLIC_APP_URL
return <div>{appUrl}</div>
}

分离服务端和客户端#

lib/env.ts
import { z } from 'zod'
// 服务端环境变量
const serverEnvSchema = z.object({
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(32),
AUTH_SECRET: z.string().min(32),
})
// 客户端环境变量
const clientEnvSchema = z.object({
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
})
// 服务端变量(仅在服务端验证)
export const serverEnv = (() => {
if (typeof window !== 'undefined') {
throw new Error('serverEnv cannot be accessed on the client')
}
return serverEnvSchema.parse(process.env)
})()
// 客户端变量
export const clientEnv = clientEnvSchema.parse({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
})

运行时配置#

next.config.ts 环境变量#

next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
env: {
// 构建时注入
BUILD_TIME: new Date().toISOString(),
GIT_COMMIT: process.env.VERCEL_GIT_COMMIT_SHA || 'development',
},
}
export default config

公共运行时配置#

next.config.ts
const config: NextConfig = {
publicRuntimeConfig: {
// 客户端和服务端都可访问
staticFolder: '/static',
},
serverRuntimeConfig: {
// 仅服务端可访问
mySecret: process.env.MY_SECRET,
},
}
// 使用
import getConfig from 'next/config'
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()
// 服务端
console.log(serverRuntimeConfig.mySecret)
// 客户端和服务端
console.log(publicRuntimeConfig.staticFolder)

多环境配置#

开发环境#

.env.development
DATABASE_URL="postgresql://localhost:5432/myapp_dev"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
LOG_LEVEL="debug"

生产环境#

.env.production
DATABASE_URL="postgresql://prod-server:5432/myapp"
NEXT_PUBLIC_APP_URL="https://myapp.com"
LOG_LEVEL="error"

测试环境#

.env.test
DATABASE_URL="postgresql://localhost:5432/myapp_test"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
LOG_LEVEL="warn"

条件逻辑#

lib/config.ts
const isDevelopment = process.env.NODE_ENV === 'development'
const isProduction = process.env.NODE_ENV === 'production'
const isTest = process.env.NODE_ENV === 'test'
export const config = {
isDevelopment,
isProduction,
isTest,
apiUrl: isProduction ? 'https://api.myapp.com' : 'http://localhost:4000',
features: {
debugMode: isDevelopment,
analytics: isProduction,
},
}

安全最佳实践#

不要提交敏感信息#

.gitignore
.env.local
.env.*.local

提供示例文件#

Terminal window
# .env.example(提交到 git)
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
API_SECRET="your-api-secret-here"
NEXT_PUBLIC_APP_URL="http://localhost:3000"

验证必需变量#

lib/env.ts
const requiredEnvVars = ['DATABASE_URL', 'API_SECRET', 'AUTH_SECRET'] as const
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`)
}
}

不要在客户端暴露敏感信息#

// ❌ 错误:敏感信息暴露给客户端
const NEXT_PUBLIC_API_SECRET = 'secret' // 不要这样做!
// ✅ 正确:通过 API 路由访问
// app/api/protected/route.ts
const secret = process.env.API_SECRET // 仅服务端可访问

部署平台配置#

Vercel#

Terminal window
# 通过 CLI 设置
vercel env add DATABASE_URL
# 或在仪表板中设置
# Settings > Environment Variables
// 使用 Vercel 特有变量
const commitSha = process.env.VERCEL_GIT_COMMIT_SHA
const deploymentUrl = process.env.VERCEL_URL

Docker#

# Dockerfile
FROM node:20-alpine
# 构建参数
ARG DATABASE_URL
ARG NEXT_PUBLIC_APP_URL
# 设置环境变量
ENV DATABASE_URL=$DATABASE_URL
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
# ...
Terminal window
# 构建时传入
docker build \
--build-arg DATABASE_URL="..." \
--build-arg NEXT_PUBLIC_APP_URL="https://myapp.com" \
-t myapp .

Docker Compose#

docker-compose.yml
services:
app:
build: .
environment:
- DATABASE_URL=postgresql://db:5432/myapp
- API_SECRET=${API_SECRET}
env_file:
- .env.production

配置文件组织#

集中管理#

config/index.ts
import { env } from '@/lib/env'
export const config = {
app: {
name: 'My App',
url: env.NEXT_PUBLIC_APP_URL,
version: process.env.npm_package_version || '0.0.0',
},
database: {
url: env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE || '10'),
},
auth: {
secret: env.AUTH_SECRET,
sessionMaxAge: 60 * 60 * 24 * 7, // 7 天
},
email: {
from: process.env.EMAIL_FROM || 'noreply@myapp.com',
replyTo: process.env.EMAIL_REPLY_TO,
},
features: {
enableRegistration: process.env.ENABLE_REGISTRATION !== 'false',
maintenanceMode: process.env.MAINTENANCE_MODE === 'true',
},
}

使用配置#

app/api/auth/route.ts
import { config } from '@/config'
export async function POST() {
if (config.features.maintenanceMode) {
return Response.json({ error: '系统维护中' }, { status: 503 })
}
// ...
}

动态配置#

从数据库加载#

lib/dynamic-config.ts
import { cache } from 'react'
import { db } from '@/lib/db'
export const getConfig = cache(async () => {
const settings = await db.setting.findMany()
return settings.reduce(
(acc, setting) => {
acc[setting.key] = setting.value
return acc
},
{} as Record<string, string>
)
})

特性开关#

lib/features.ts
import { cache } from 'react'
import { db } from '@/lib/db'
interface FeatureFlags {
newDashboard: boolean
betaFeatures: boolean
darkMode: boolean
}
export const getFeatureFlags = cache(async (): Promise<FeatureFlags> => {
const flags = await db.featureFlag.findMany()
return {
newDashboard:
flags.find((f) => f.name === 'newDashboard')?.enabled ?? false,
betaFeatures:
flags.find((f) => f.name === 'betaFeatures')?.enabled ?? false,
darkMode: flags.find((f) => f.name === 'darkMode')?.enabled ?? true,
}
})
app/dashboard/page.tsx
import { getFeatureFlags } from '@/lib/features'
import { NewDashboard } from '@/components/NewDashboard'
import { OldDashboard } from '@/components/OldDashboard'
export default async function DashboardPage() {
const flags = await getFeatureFlags()
return flags.newDashboard ? <NewDashboard /> : <OldDashboard />
}

常见问题#

🤔 Q: 环境变量修改后不生效?

需要重启开发服务器。环境变量在启动时读取。

Terminal window
# 停止后重新启动
pnpm dev

🤔 Q: 如何在构建时使用不同的环境变量?

Terminal window
# 使用特定环境文件
NODE_ENV=production pnpm build
# 或直接指定
DATABASE_URL="..." pnpm build

🤔 Q: 客户端能访问 process.env 吗?

只能访问 NEXT_PUBLIC_ 前缀的变量。其他变量在客户端打包时会被替换为 undefined


下一篇将介绍测试策略,学习如何测试 Next.js 应用。

-EOF-