🙋 API 密钥、数据库连接串这些敏感信息怎么管理?如何区分开发和生产环境?
环境变量基础#
.env 文件#
.env # 所有环境,提交到 git.env.local # 本地覆盖,不提交.env.development # 开发环境.env.production # 生产环境.env.test # 测试环境加载优先级(高到低):
.env.development.local.env.local.env.development.env定义环境变量#
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_URLconst secret = process.env.API_SECRET
// 客户端(仅 NEXT_PUBLIC_ 前缀)const appUrl = process.env.NEXT_PUBLIC_APP_URL
// ❌ 客户端无法访问const secret = process.env.API_SECRET // undefined类型安全#
环境变量验证#
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使用类型安全的环境变量#
import { env } from '@/lib/env'
export default function Page() { // 类型安全,IDE 自动补全 const appUrl = env.NEXT_PUBLIC_APP_URL
return <div>{appUrl}</div>}分离服务端和客户端#
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 环境变量#
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公共运行时配置#
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)多环境配置#
开发环境#
DATABASE_URL="postgresql://localhost:5432/myapp_dev"NEXT_PUBLIC_APP_URL="http://localhost:3000"LOG_LEVEL="debug"生产环境#
DATABASE_URL="postgresql://prod-server:5432/myapp"NEXT_PUBLIC_APP_URL="https://myapp.com"LOG_LEVEL="error"测试环境#
DATABASE_URL="postgresql://localhost:5432/myapp_test"NEXT_PUBLIC_APP_URL="http://localhost:3000"LOG_LEVEL="warn"条件逻辑#
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, },}安全最佳实践#
不要提交敏感信息#
.env.local.env.*.local提供示例文件#
# .env.example(提交到 git)DATABASE_URL="postgresql://user:password@localhost:5432/mydb"API_SECRET="your-api-secret-here"NEXT_PUBLIC_APP_URL="http://localhost:3000"验证必需变量#
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.tsconst secret = process.env.API_SECRET // 仅服务端可访问部署平台配置#
Vercel#
# 通过 CLI 设置vercel env add DATABASE_URL
# 或在仪表板中设置# Settings > Environment Variables// 使用 Vercel 特有变量const commitSha = process.env.VERCEL_GIT_COMMIT_SHAconst deploymentUrl = process.env.VERCEL_URLDocker#
# DockerfileFROM node:20-alpine
# 构建参数ARG DATABASE_URLARG NEXT_PUBLIC_APP_URL
# 设置环境变量ENV DATABASE_URL=$DATABASE_URLENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
# ...# 构建时传入docker build \ --build-arg DATABASE_URL="..." \ --build-arg NEXT_PUBLIC_APP_URL="https://myapp.com" \ -t myapp .Docker Compose#
services: app: build: . environment: - DATABASE_URL=postgresql://db:5432/myapp - API_SECRET=${API_SECRET} env_file: - .env.production配置文件组织#
集中管理#
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', },}使用配置#
import { config } from '@/config'
export async function POST() { if (config.features.maintenanceMode) { return Response.json({ error: '系统维护中' }, { status: 503 }) }
// ...}动态配置#
从数据库加载#
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> )})特性开关#
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, }})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: 环境变量修改后不生效?
需要重启开发服务器。环境变量在启动时读取。
# 停止后重新启动pnpm dev🤔 Q: 如何在构建时使用不同的环境变量?
# 使用特定环境文件NODE_ENV=production pnpm build
# 或直接指定DATABASE_URL="..." pnpm build🤔 Q: 客户端能访问 process.env 吗?
只能访问 NEXT_PUBLIC_ 前缀的变量。其他变量在客户端打包时会被替换为 undefined。
下一篇将介绍测试策略,学习如何测试 Next.js 应用。
-EOF-