03. 环境变量管理

环境变量是区分开发、测试、生产环境的关键。Next.js 提供了强大的环境变量管理机制,但也有一些安全陷阱需要注意。


1. 环境变量文件

Next.js 支持多种环境变量文件,按优先级从高到低:

文件名使用场景优先级
.env.local本地开发,不应该提交到 Git最高
.env.development开发环境
.env.production生产环境
.env所有环境通用最低

文件加载顺序

Next.js 会按以下顺序加载环境变量:

  1. .env(所有环境)
  2. .env.development.env.production(根据 NODE_ENV
  3. .env.local(所有环境,但优先级最高)

重要:后加载的变量会覆盖先加载的变量。


2. 客户端 vs 服务端变量

这是 Next.js 环境变量管理的核心概念。

服务端变量(默认)

所有环境变量默认只在服务端可用,不会暴露到客户端。

# .env.local
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=super-secret-key-123
// ✅ 服务端组件可以使用
export default async function Page() {
  const dbUrl = process.env.DATABASE_URL // ✅ 可以访问
  return <div>...</div>
}
 
// ❌ 客户端组件不能使用
'use client'
export default function ClientComponent() {
  const dbUrl = process.env.DATABASE_URL // ❌ undefined
  return <div>...</div>
}

客户端变量(需要前缀)

如果变量需要在客户端使用,必须加上 NEXT_PUBLIC_ 前缀。

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=My App
// ✅ 客户端组件可以使用
'use client'
export default function ClientComponent() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL // ✅ 可以访问
  return <div>{apiUrl}</div>
}

⚠️ 安全警告NEXT_PUBLIC_ 前缀的变量会被打包到客户端 JS 中,任何人都可以在浏览器中看到。永远不要把敏感信息(如 API 密钥、数据库密码)放在 NEXT_PUBLIC_ 变量中。


3. 实战案例

案例 1:API 配置

场景:开发环境使用本地 API,生产环境使用远程 API。

# .env.development
NEXT_PUBLIC_API_URL=http://localhost:3001/api
API_SECRET_KEY=dev-secret-key
 
# .env.production
NEXT_PUBLIC_API_URL=https://api.production.com
API_SECRET_KEY=prod-secret-key-xxx

使用

// 客户端组件
'use client'
export default function ApiComponent() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL
  
  const fetchData = async () => {
    const res = await fetch(`${apiUrl}/users`)
    return res.json()
  }
  
  return <button onClick={fetchData}>获取数据</button>
}
 
// 服务端组件
export default async function ServerPage() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL
  const secretKey = process.env.API_SECRET_KEY // ✅ 服务端可用
  
  const res = await fetch(`${apiUrl}/users`, {
    headers: {
      'Authorization': `Bearer ${secretKey}` // 安全:密钥不会暴露到客户端
    }
  })
  
  const data = await res.json()
  return <div>{/* 渲染数据 */}</div>
}

案例 2:数据库连接

场景:数据库连接字符串不应该暴露到客户端。

# .env.local(不提交到 Git)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
// ✅ 只能在服务端使用
// app/api/users/route.ts
import { PrismaClient } from '@prisma/client'
 
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL, // ✅ 服务端安全
    },
  },
})
 
export async function GET() {
  const users = await prisma.user.findMany()
  return Response.json(users)
}

案例 3:第三方服务配置

场景:集成 Stripe 支付(需要公钥和私钥)。

# .env.local
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
// 客户端:使用公钥
'use client'
import { loadStripe } from '@stripe/stripe-js'
 
const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
)
 
export default function CheckoutButton() {
  const handleCheckout = async () => {
    const stripe = await stripePromise
    // 使用公钥初始化 Stripe
  }
  
  return <button onClick={handleCheckout}>支付</button>
}
 
// 服务端:使用私钥
// app/api/checkout/route.ts
import Stripe from 'stripe'
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
})
 
export async function POST(request: Request) {
  // 使用私钥创建支付意图
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: 'usd',
  })
  
  return Response.json({ clientSecret: paymentIntent.client_secret })
}

4. 类型安全的环境变量

创建类型定义

// src/types/env.d.ts
namespace NodeJS {
  interface ProcessEnv {
    // 客户端变量
    NEXT_PUBLIC_API_URL: string
    NEXT_PUBLIC_APP_NAME: string
    
    // 服务端变量
    DATABASE_URL: string
    API_SECRET_KEY: string
  }
}

使用验证库(推荐)

使用 zod 验证环境变量,确保类型安全:

npm install zod
// src/lib/env.ts
import { z } from 'zod'
 
const envSchema = z.object({
  // 客户端变量
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_APP_NAME: z.string(),
  
  // 服务端变量
  DATABASE_URL: z.string().url(),
  API_SECRET_KEY: z.string().min(1),
})
 
// 验证并导出
export const env = envSchema.parse({
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  DATABASE_URL: process.env.DATABASE_URL,
  API_SECRET_KEY: process.env.API_SECRET_KEY,
})
 
// 使用
// import { env } from '@/lib/env'
// const apiUrl = env.NEXT_PUBLIC_API_URL // 类型安全

5. .gitignore 配置

确保敏感信息不会被提交到 Git:

# .gitignore
 
# 环境变量文件
.env*.local
.env.local
.env.development.local
.env.test.local
.env.production.local
 
# 但保留示例文件
!.env.example

创建 .env.example 作为模板

# .env.example
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=your-secret-key

团队成员可以复制 .env.example.env.local 并填入实际值。


6. 运行时环境变量

在 Vercel 中配置

  1. 进入项目设置
  2. 找到 “Environment Variables”
  3. 添加变量(会自动区分开发/预览/生产环境)

在 Docker 中配置

# Dockerfile
FROM node:18-alpine
 
WORKDIR /app
 
COPY package*.json ./
RUN npm install
 
COPY . .
 
# 构建时传入环境变量
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
 
RUN npm run build
 
CMD ["npm", "start"]
# 运行容器时传入
docker build --build-arg NEXT_PUBLIC_API_URL=https://api.example.com .

7. 常见错误和解决方案

错误 1:客户端访问服务端变量

// ❌ 错误
'use client'
export default function Component() {
  const secret = process.env.API_SECRET_KEY // undefined
  return <div>{secret}</div>
}
 
// ✅ 正确:通过 API 路由获取
'use client'
export default function Component() {
  const [data, setData] = useState(null)
  
  useEffect(() => {
    fetch('/api/secret-data')
      .then(res => res.json())
      .then(setData)
  }, [])
  
  return <div>{data}</div>
}

错误 2:忘记 NEXT_PUBLIC_ 前缀

// ❌ 错误
'use client'
const apiUrl = process.env.API_URL // undefined
 
// ✅ 正确
const apiUrl = process.env.NEXT_PUBLIC_API_URL

错误 3:在构建时使用运行时变量

// ❌ 错误:构建时 NEXT_PUBLIC_API_URL 可能不存在
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'default'
 
// ✅ 正确:使用验证
import { env } from '@/lib/env'
const apiUrl = env.NEXT_PUBLIC_API_URL

8. 实战练习:配置多环境项目

需求

创建一个支持开发、测试、生产三环境的项目配置。

实现步骤

步骤 1:创建环境变量文件

# .env.development
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_ENV=development
 
# .env.production
NEXT_PUBLIC_API_URL=https://api.production.com
NEXT_PUBLIC_ENV=production
 
# .env.local(不提交)
DATABASE_URL=postgresql://localhost:5432/devdb

步骤 2:创建类型定义

// src/types/env.d.ts
namespace NodeJS {
  interface ProcessEnv {
    NEXT_PUBLIC_API_URL: string
    NEXT_PUBLIC_ENV: 'development' | 'production' | 'test'
    DATABASE_URL: string
  }
}

步骤 3:使用环境变量

// src/app/page.tsx
export default function Home() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL
  const env = process.env.NEXT_PUBLIC_ENV
  
  return (
    <div>
      <h1>当前环境:{env}</h1>
      <p>API 地址:{apiUrl}</p>
    </div>
  )
}

9. 总结

本章我们学习了:

  1. ✅ 环境变量文件的优先级和加载顺序
  2. ✅ 客户端变量(NEXT_PUBLIC_)和服务端变量的区别
  3. ✅ 如何安全地管理敏感信息
  4. ✅ 类型安全的环境变量验证
  5. ✅ 多环境配置最佳实践

安全要点

  • 🔒 永远不要把敏感信息放在 NEXT_PUBLIC_ 变量中
  • 🔒 .env.local 不要提交到 Git
  • 🔒 使用 zod 验证环境变量,确保类型安全

下一步:在下一章,我们将通过实战创建多个页面,深入学习 Next.js 的路由系统。