在开发 Node.js 项目时,process.env.XXX 可以在很多地方设置,令人困惑。本文从底层原理出发,系统梳理所有设置方式、优先级和最佳实践。
本质:环境变量是什么
process.env 只是操作系统进程的环境变量表,Node.js 只是读取它。设置方式多,是因为不同场景下谁能控制这张表是不同的。
操作系统进程启动
└─ 继承父进程的环境变量(shell)
└─ 启动命令可以追加/覆盖
└─ 代码运行时可以再修改 process.env
五种设置方式
1. 命令行直接传入(最常见)
NODE_ENV=production npm run start
# 多个变量
MRN_HARMONY=true NODE_ENV=production npm run start
2. package.json 的 scripts 里写死
{
"scripts": {
"start": "NODE_ENV=production node server.js",
"start:harmony": "cross-env MRN_HARMONY=true node server.js"
}
}
3. .env 文件(配合 dotenv)
# .env 文件
MRN_HARMONY=true
PORT=3000
代码里 require('dotenv').config(),或由框架自动加载(Next.js、Vite 等)。
4. CI/CD 平台配置
在 Jenkins、GitHub Actions 或公司内部发布平台上配置,构建/运行时自动注入。
5. Shell 全局配置
# ~/.zshrc 或 ~/.bashrc
export MRN_HARMONY=true
优先级(后覆盖前)
低 ←————————————————————————————→ 高
Shell 全局 .env 文件 package.json 命令行传入 代码里赋值
~/.zshrc .env scripts 里写 MY=1 npm process.env.MY=1
注意:dotenv 默认不覆盖已存在的变量(命令行传入的优先),这是特意设计的——越靠近运行时、越具体的配置,优先级越高。
什么情况放在哪
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 本地开发,个人配置不同 |
.env.local(gitignore) |
个人差异,不能提交 |
| 本地开发,团队统一默认值 |
.env 或 .env.development
|
提交进仓库,开箱即用 |
| 生产环境密钥(AK/SK) | CI/CD 平台配置 | 不能进代码仓库 |
| 区分不同环境(dev/test/prod) |
.env.production 等分环境文件 |
构建时由框架自动选择 |
| 临时调试、一次性测试 | 命令行 MY=1 npm run xxx
|
不污染任何配置文件 |
| Feature Flag | CI 平台 + 本地命令行 | 灰度控制,不同环境不同值 |
进阶:一个项目的典型分层
项目根目录/
├── .env # 基础默认值,提交 git(不含敏感信息)
├── .env.development # 开发环境覆盖值,提交 git
├── .env.production # 生产环境覆盖值,提交 git
├── .env.local # 本地个人覆盖,gitignore!
└── .env.example # 列出所有变量名但不填真实值,相当于"接口文档"
加载顺序(以 Next.js/Vite 为例,后加载的优先级高):
.env → .env.[mode] → .env.local → .env.[mode].local → 命令行
.env.example 是个好习惯——提交到 git,告诉团队有哪些变量需要配置。
构建时变量 vs 运行时变量(最容易踩坑)
运行时变量 构建时变量
process.env.PORT process.env.NEXT_PUBLIC_API_URL
↓ ↓
服务器运行期间读取 打包时被 bundler 直接替换成字符串
可以动态改 改了必须重新打包
前端框架对”暴露给浏览器”的变量有特殊前缀约定(因为浏览器里没有 process):
NEXT_PUBLIC_xxx # Next.js
VITE_xxx # Vite
REACT_APP_xxx # CRA(已过时)
原理:打包时 bundler 把 process.env.VITE_API_URL 文本替换成实际值字符串。没加前缀的变量不会被替换,在浏览器里是 undefined。
跨平台问题
# Mac/Linux 可以直接用
NODE_ENV=production node app.js
# Windows 不行!需要 cross-env
npx cross-env NODE_ENV=production node app.js
这就是为什么很多项目 package.json 里装了 cross-env。
启动时校验(生产项目必备)
直接用 process.env.XXX 的问题:缺了变量运行时才报错,且报错位置很深。推荐在启动时统一校验:
// env.ts - 启动时就报错,而不是运行到某个功能才发现
import { z } from 'zod'
const schema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']),
})
export const env = schema.parse(process.env)
// 后续代码只 import env,不直接用 process.env
Docker 环境
# Dockerfile 里设默认值
ENV NODE_ENV=production
# docker run 时覆盖
# docker run -e NODE_ENV=staging my-app
# docker-compose.yml
services:
app:
env_file: .env.production # 指定文件
environment:
- MRN_HARMONY=true # 单独追加
安全边界(最重要的原则)
永远不要把以下内容放进代码仓库:
- 数据库密码
- API 密钥 / Secret
- 私钥证书
永远不要用带 NEXT_PUBLIC_ / VITE_ 前缀的变量存敏感信息:
加了前缀 = 打进前端 bundle = 用户打开 DevTools 就能看到。
决策树
这个变量是什么?
│
├─ 敏感信息(密码/密钥)
│ └─ 生产:CI/KMS 注入,本地:.env.local(gitignore)
│
├─ 区分环境的配置(API 地址、Feature Flag)
│ ├─ 需要进浏览器?→ 加框架前缀(VITE_/NEXT_PUBLIC_)
│ └─ 仅服务端?→ .env.[environment],可以提交 git
│
├─ 本地个人偏好(开启某个调试功能)
│ └─ .env.local 或命令行传入
│
└─ 默认值 / 文档示例
└─ .env.example(提交 git,告诉团队有哪些变量)