2、环境变量基础:Node.js 中的 process.env,.env 文件规范与 dotenv 库的使用

聊到多环境配置,第一个绕不开的概念就是环境变量。说白了,环境变量就是操作系统或运行环境里的一组键值对,用来告诉程序「你现在在哪儿、该用哪套配置」。

我记得刚入行那会儿,有个项目上线后 API 地址死活不对。查了半天,发现代码里写死了 localhost:3000。嗯,从那以后,我再也不敢把配置硬编码在代码里了。

Node.js 中的 process.env

在 Node.js 里,环境变量通过 process.env 访问。这是一个全局对象,包含了当前进程的所有环境变量。

// 最简单的用法
console.log(process.env.NODE_ENV);
// 输出可能是 'development' 或 'production'

// 设置默认值
const env = process.env.NODE_ENV || 'development';
console.log(`当前环境: ${env}`);

你可以在启动 Node.js 进程时传入环境变量:

# Linux / macOS
NODE_ENV=production node app.js

# Windows (CMD)
set NODE_ENV=production && node app.js

# Windows (PowerShell)
$env:NODE_ENV="production"; node app.js

我个人习惯用 cross-env 这个库,跨平台兼容,省心不少。

核心要点:process.env 是 Node.js 原生支持的,不需要额外安装任何包。但直接用它管理多环境配置,会有点「裸奔」的感觉——变量多了就乱,团队协作也容易出问题。

.env 文件规范

为了解决配置混乱的问题,社区逐渐形成了 .env 文件的约定。这个规范最早来自 Ruby 的 dotenv 项目,后来被前端社区广泛采纳。

一个典型的 .env 文件长这样:

# .env 文件示例
# 这是注释,以 # 开头

APP_NAME=MyApp
NODE_ENV=development
PORT=3000
API_BASE_URL=http://localhost:8080/api
DB_HOST=localhost
DB_PORT=5432
DB_USER=admin
DB_PASS=secret123

规范上有几个要点,我踩过坑,给你列一下:

  • 键名全大写,单词间用下划线分隔。比如 API_BASE_URL,别写成 apiBaseUrl
  • 值不加引号,除非值本身包含空格或特殊字符。加了引号,引号也会被当成值的一部分。
  • 注释用 #,可以单独一行,也可以跟在值后面。
  • 空行会被忽略,用来分组挺方便的。
  • 不要提交 .env 文件到 Git,尤其是包含密码、密钥的。应该提交一个 .env.example 作为模板。

我曾经犯过的错:有次把 .env 文件提交到了 GitHub 公开仓库,里面包含了数据库密码。虽然几分钟后就删了,但已经被爬虫抓到了。嗯,那晚我改了一整夜的密码。

多环境场景下,通常会这样组织文件:

文件名 用途 是否提交 Git
.env 默认配置,所有环境共享
.env.development 开发环境专用 是(示例值)
.env.production 生产环境专用 是(示例值)
.env.local 本地覆盖,优先级最高
.env.example 模板文件,给新同事参考

你想想看,如果每个环境都有一套独立的配置文件,部署时只需要加载对应的文件就行。是不是比在代码里写一堆 if-else 优雅多了?

dotenv 库的使用

Node.js 默认不会自动加载 .env 文件。你需要用 dotenv 这个库来「读取并注入」环境变量。

安装很简单:

npm install dotenv

基本用法:

// 在入口文件最顶部引入
require('dotenv').config();

// 现在 process.env 里就有 .env 文件的内容了
console.log(process.env.APP_NAME);
console.log(process.env.PORT);

dotenv 的 config() 方法默认读取项目根目录下的 .env 文件。你也可以指定路径:

// 加载特定环境的配置文件
require('dotenv').config({ path: '.env.production' });

// 或者加载多个文件,后面的会覆盖前面的
require('dotenv').config({ path: '.env.defaults' });
require('dotenv').config({ path: '.env.local' });

我的小技巧:在项目入口文件里,我会这样组织加载顺序:先加载 .env(公共配置),再加载 .env.local(本地覆盖)。这样既保证了基础配置,又允许开发者本地调试时覆盖某些值。

实际项目中,我通常会封装一个配置加载函数:

// config/index.js
const dotenv = require('dotenv');
const path = require('path');

// 确定当前环境
const env = process.env.NODE_ENV || 'development';

// 加载顺序:基础配置 → 环境配置 → 本地覆盖
const envFiles = [
  '.env',                    // 所有环境共享
  `.env.${env}`,             // 当前环境专用
  '.env.local',              // 本地覆盖(不提交 Git)
];

envFiles.forEach(file => {
  const filePath = path.resolve(process.cwd(), file);
  const result = dotenv.config({ path: filePath });
  
  if (result.error) {
    // 文件不存在就跳过,不报错
    if (result.error.code === 'ENOENT') {
      console.warn(`⚠️ 配置文件 ${file} 不存在,跳过`);
    } else {
      console.error(`❌ 加载 ${file} 失败:`, result.error);
    }
  } else {
    console.log(`✅ 已加载 ${file}`);
  }
});

// 导出统一的配置对象
module.exports = {
  appName: process.env.APP_NAME || 'MyApp',
  port: parseInt(process.env.PORT, 10) || 3000,
  apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8080',
  db: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 5432,
    user: process.env.DB_USER || 'root',
    password: process.env.DB_PASS || '',
  },
  isDev: env === 'development',
  isProd: env === 'production',
};

这样封装之后,项目里其他地方只需要 const config = require('./config') 就能拿到所有配置。而且类型转换、默认值都在一个地方处理,维护起来特别方便。

为什么推荐这种写法?因为 process.env 里所有值都是字符串。你想想看,PORT=3000 取出来是字符串 '3000',不是数字。不做类型转换,后面做端口判断时很容易出 bug。我就在这上面吃过亏——字符串比较 '3000' === 3000 返回 false,排查了半天。

实战中的注意事项

最后分享几个我在项目中积累的经验:

  • 不要在代码里直接访问 process.env,而是通过一个统一的配置模块。这样哪天换配置管理方式,只需要改一个文件。
  • .env 文件不要提交到 Git,但一定要提供 .env.example。新同事拉下代码,复制一份 .env.example 改成 .env 就能跑起来。
  • 敏感信息(密码、密钥、Token)不要放在 .env 文件里提交到任何地方。生产环境的敏感信息应该通过 CI/CD 的变量注入或者密钥管理服务来提供。
  • dotenv 只在开发环境用。生产环境通常由运维平台或容器编排工具直接注入环境变量,不需要 .env 文件。

好了,这一章的内容就这些。环境变量看似简单,但用好了能让你的项目配置管理井井有条。下一章我们聊聊如何在 Vite 和 Webpack 中使用环境变量,那又是另一番天地了。