环境变量与配置
这一课你将能做到
- 用
process.env 读取环境变量,并说明它「全字符串、按进程隔离」的两个特性
- 用
node --env-file 或 dotenv 加载 .env 文件,把密钥、端口从代码里彻底挪出去
- 搭一个按环境(dev/test/prod)切换配置的 config 模块,并知道哪些文件绝不能提交 git
为什么要学
代码里写死 const dbPassword = 'admin123' 会发生什么?提交 git 就是公开处刑——爬虫扫到你的仓库,密码几小时内就会被尝试连接。行业解法是 12-Factor 的配置分离原则:代码同一份,配置按环境注入,密钥只存在于部署环境里。
人话:环境变量就是「操作系统级别的小储物柜」,你启动进程前把值放进去,进程用 process.env 取。开发时值在 .env 文件里(不提交 git),生产时值在部署平台的配置面板里。这课内容不多,但它是所有真实项目的「入场券」——不会这课,项目连安全地放到 GitHub 上都做不到。
先建立图像
┌─ 开发环境 ─────────────────────────────┐
│ .env 文件(git 忽略) │
│ PORT=3000 │
│ DB_HOST=localhost │
│ 启动: node --env-file=.env server.js │
│ └──► 注入 process.env │
└────────────────────────────────────────┘
┌─ 生产环境 ─────────────────────────────┐
│ 部署平台配置面板 / CI 变量 / K8s Secret│
│ (同样注入 process.env,代码零改动) │
└────────────────────────────────────────┘
代码里只写: const port = Number(process.env.PORT ?? 3000)
同一份代码,值从外面来——这就是「配置与代码分离」。
跟着做
步骤 1:认识 process.env
新建 node-env 目录,npm init -y,新建 show-env.js:
// show-env.js
console.log('系统 PATH 存在吗:', Boolean(process.env.PATH));
console.log('当前用户(Windows):', process.env.USERNAME);
console.log('自定义变量 MY_FLAG:', process.env.MY_FLAG); // 没设置就是 undefined
console.log('PORT 原始值:', JSON.stringify(process.env.PORT));
终端里先注入再运行(PowerShell):
$env:PORT="3000"; node show-env.js
# 自定义变量 MY_FLAG: undefined
# PORT 原始值: "3000" ← 注意是字符串,带引号!
Linux/macOS(bash)写法:
PORT=3000 MY_FLAG=1 node show-env.js
两个铁律当场验证:没设置的变量是 undefined;设置的值永远是字符串("3000" 不是 3000)。
步骤 2:写一个健壮的取值工具
新建 env.js:
// env.js —— 把"取环境变量 + 类型转换 + 默认值"收编成三个小函数
function envStr(key, defaultValue) {
const v = process.env[key];
return v === undefined || v === '' ? defaultValue : v;
}
function envInt(key, defaultValue) {
const v = Number.parseInt(process.env[key] ?? '', 10);
return Number.isNaN(v) ? defaultValue : v;
}
function envBool(key, defaultValue) {
const v = process.env[key];
if (v === undefined) return defaultValue;
return v === '1' || v.toLowerCase() === 'true';
}
module.exports = { envStr, envInt, envBool };
// check-config.js
const { envStr, envInt, envBool } = require('./env');
const port = envInt('PORT', 3000);
const host = envStr('DB_HOST', 'localhost');
const debug = envBool('DEBUG', false);
console.log({ port, host, debug });
node check-config.js
# { port: 3000, host: 'localhost', debug: false }
$env:PORT="4000"; $env:DEBUG="true"; node check-config.js
# { port: 4000, host: 'localhost', debug: true }
直接在业务代码里到处写 process.env.XXX 是配置散落的开始;收敛到一个模块,类型和默认值就都有了唯一出处。
步骤 3:用 --env-file 加载 .env(Node 20.6+ 内置)
PORT=3000
DB_HOST=localhost
DB_USER=dev_user
DB_PASSWORD=dev-secret-do-not-commit
DEBUG=true
// server.js
const { envInt, envStr, envBool } = require('./env');
const port = envInt('PORT', 3000);
console.log(`启动于端口 ${port}`);
console.log('DB 地址:', envStr('DB_HOST', 'unset'));
console.log('DEBUG:', envBool('DEBUG', false));
node --env-file=.env server.js
# 启动于端口 3000
# DB 地址: localhost
# DEBUG: true
--env-file 是 Node 20.6+ 的内置能力,零依赖。补充两个实用变体:
node --env-file=.env.development server.js # 指定文件
node --env-file-if-missing=.env server.js # 文件不存在不报错(进 CI 用)
老项目里常见 dotenv 包(npm i dotenv 后在入口第一行 require('dotenv').config()),效果相同——读它就是认识老代码。
步骤 4:把 .env 挡在 git 门外
.env 含密钥,必须让 git 无视它。新建 .gitignore:
node_modules/
.env
.env.local
.env.*.local
同时给协作者一个模板 .env.example(提交 git,值全用占位符):
PORT=3000
DB_HOST=localhost
DB_USER=
DB_PASSWORD=
DEBUG=false
git check-ignore .env # 输出 .env 说明已被忽略,配置生效
流程:新人克隆项目 → 复制 .env.example 为 .env → 填上本地值 → 跑起来。.env.example 是团队约定,.env 是个人秘密,两者永远分家。
步骤 5:按环境组织的 config 模块
真实项目不只有一个环境。做法:.env 提供基础值,代码里按 NODE_ENV 做环境分支。
// config.js —— 全项目唯一的配置出口
const { envStr, envInt, envBool } = require('./env');
const nodeEnv = envStr('NODE_ENV', 'development');
const isProd = nodeEnv === 'production';
const defaults = {
development: { logLevel: 'debug', dbHost: 'localhost', dbPort: 3306 },
test: { logLevel: 'warn', dbHost: 'localhost', dbPort: 3307 },
production: { logLevel: 'info', dbHost: '', dbPort: 3306 },
};
const d = defaults[nodeEnv] ?? defaults.development; // 未知环境回退到 development
const conf = {
nodeEnv,
isProd,
port: envInt('PORT', 3000),
logLevel: envStr('LOG_LEVEL', d.logLevel),
db: {
host: envStr('DB_HOST', d.dbHost),
port: envInt('DB_PORT', d.dbPort),
user: envStr('DB_USER', ''),
password: envStr('DB_PASSWORD', ''),
},
debug: envBool('DEBUG', !isProd),
};
module.exports = Object.freeze(conf); // 冻结,防止运行期被意外改写
// server.js 追加
const config = require('./config');
console.log(`环境=${config.nodeEnv} 日志级别=${config.logLevel} DB=${config.db.host}:${config.db.port}`);
node --env-file=.env server.js
# 环境=development 日志级别=debug DB=localhost:3306
两个设计点:Object.freeze 让配置只读;生产环境敏感项强制从 env 取值(默认值为空串),起服务时缺失就应立刻报错,而不是用兜底值带病上线。
讲清楚要点
- process.env 的一切值都是字符串:
"0"、"false"、"" 都是「有值」。判断开关要么 === 'true',要么统一走 envBool 这类转换工具。
- 优先级要清楚:真实的环境变量 >
--env-file 注入的值。也就是说系统里已有 PORT 时,.env 里的 PORT 不会覆盖它(--env-file 不覆盖已存在变量),这让 CI 里可以「外面传值压过文件默认值」。
- NODE_ENV 是社区约定,不是语法:Node 本身不关心它的值,是框架和库约定
production/development/test 三档。node --env-file=.env NODE_ENV=production app.js(bash)或在 .env 里写 NODE_ENV=production 都行。
- .env 只进开发机,密钥只进部署平台:生产配置由部署系统(平台面板、CI/CD 变量、K8s Secret)注入,代码与
.env 都不背密钥。
- 配置模块是唯一出口:所有
process.env 的读取收敛到一个 config 文件(做类型转换、默认值、freeze),业务代码只 import config——换部署方式时只改一处。
你可能会懵的地方
-
环境变量到底「活」在哪?
活在进程的内存里,从父进程(shell/部署系统)继承。PowerShell $env:X="1" 只影响当前会话及其子进程;换个终端就没有。永久生效要走系统设置或部署平台。
-
.env 和 .env.example 都要提交吗?
.env 不提交(有真实密钥);.env.example 提交(只有字段名和占位值,是给协作者的说明书)。git 提交前用 git status 确认 .env 没混进来。
-
--env-file 报 ENOENT 怎么办?
文件不存在默认报错。CI/生产没有 .env 文件属正常,用 --env-file-if-missing=.env(Node 20.12+)或生产上不传该参数,让值走部署平台注入。
-
NODE_ENV 和自造的 APP_ENV 冲突吗?
不冲突但别双轨。社区生态(Express、很多库)认 NODE_ENV,统一用它;特殊需求(同一进程按租户切换)再造独立变量,但要在 config 模块里写清优先级。
-
密钥不小心提交过 git 怎么办?
当作已泄露处理:立刻在服务方轮换密钥,然后才考虑清历史(git filter-repo)。改 .gitignore 只能防未来,撤不回已推送到远端的历史。
错法对照
| 错法 | 问题 | 正解 |
|---|
if (process.env.DEBUG) 当布尔用 | 字符串 "false" 也是真值,开关失灵 | envBool 转换:v === 'true' |
if (process.env.PORT === 3000) | 与字符串 "3000" 永不相等 | Number(process.env.PORT) === 3000 |
| .env 提交到 git | 密钥永久进历史,等于公开 | .gitignore 忽略,仅提交 .env.example |
业务代码里到处 process.env.XXX | 默认值、类型转换满天飞,改一处漏三处 | 收敛到 config 模块,Object.freeze |
| 生产密钥写进 .env 打包进镜像 | 镜像被拉取即泄露 | 生产值由部署平台运行时注入 |
--env-file 在无 .env 的 CI 里直接崩 | 构建中断 | --env-file-if-missing 或按环境裁剪参数 |
卡点急救
- 设置了变量但读到 undefined:三查——注入和启动是否同进程(PowerShell 里
$env:X="1" 与 node 命令要挨着执行);变量名拼写;.env 文件里是否写了空格(PORT =3000 会解析出键 PORT 或值 3000,等号两边别加空格)。
--env-file 报 ENOENT:路径不对(相对 cwd)或文件缺失;用相对项目根的路径,CI 场景换 --env-file-if-missing。
- PowerShell 一次传多个变量:
$env:A="1"; $env:B="2"; node app.js——分号串接,或在同一行用 $env:A="1"; $env:B="2"; node app.js。
- 改了 .env 不生效:环境变量在进程启动时读取,运行中改文件无效;重启进程(node --watch 监听的是 js 文件,.env 变化需手动重启或升级 nodemon 配置)。
JSON.stringify(process.env) 打印出密钥:调试时全量打印 env 会把密钥写进日志。只打印用到的 key,或做白名单过滤。
面试常问
-
为什么要把配置放到环境变量?
12-Factor 的配置分离:同一份代码在 dev/test/prod 用不同值;密钥不进代码库和镜像,只进部署环境;换环境零代码改动。同时进程启动时注入,运行期只读,避免热改配置带来的状态混乱。
-
NODE_ENV 的作用是什么?
社区约定的运行环境标识(production/development/test),框架与库据此切换行为(Express 的视图缓存、错误详情泄露等)。它由人设置,Node 不强制校验;安全上注意「production 检测」别依赖字符串猜测,统一走 config。
-
.env 文件管理的最佳实践?
.env 本地开发用且 git 忽略;.env.example 提交作为字段清单;生产由部署平台注入真实值;Node 20.6+ 用 --env-file 加载,或用 dotenv 兼容存量项目;启动时对必需变量做存在性校验,缺失立即报错退出。
-
process.env 的值都是什么类型?怎么安全取数字/布尔?
全是字符串或 undefined。安全取值要显式转换并给默认值:Number.parseInt + isNaN 校验、布尔判断 === 'true' || === '1',封装成 envInt/envBool 统一处理。
自测三问
process.env.PORT 读到 "3000",process.env.PORT === 3000 的结果是什么?为什么?
.env 和 .env.example 分别提交 git 吗?各自的用途是什么?
node --env-file=.env app.js 时,系统里已存在的同名环境变量和 .env 里的值谁赢?
小结一句话
环境变量让「一份代码、多套配置」成立:值全是字符串要转换、.env 只进开发机、config 模块做唯一出口、密钥泄露过一次就当已泄密处理。
延伸