跳转至

环境变量管理最佳实践

预计阅读时长 : 11 分钟

在开发过程中,我们经常需要设置一些环境变量,以便于在项目中调用。

传统的环境变量管理方式,是将环境变量设置全部写入 ~/.zshrc 文件中。但这种方式太过于死板,不利于管理,也无法根据不同的项目设置不同的环境变量。

因此,推荐启用单独的 ~/.zshenv 文件配合 direnv 工具来管理环境变量,更好的满足各种个性化需求。

全局环境变量

~/.zshenv 也是 zsh 的默认配置文件,支持与 ~/.zshrc 相同的 shell 语法。

# 基础变量声明
export PATH=$HOME/bin:$PATH
export EDITOR=vim

# 条件判断
if [ -d "$HOME/.local/bin" ]; then
    export PATH="$HOME/.local/bin:$PATH"
fi

# 数组操作
export PATH_DIRS=(
    "$HOME/bin"
    "$HOME/.local/bin"
    "/usr/local/bin"
)
export PATH="${(j.:.)PATH_DIRS}:$PATH"

但它们的加载时机和使用场景有所不同:

  • ~/.zshenv 在所有情况下都会被加载,包括非交互式会话
  • ~/.zshrc 只在交互式会话中加载

虽然 ~/.zshenv 支持所有 shell 脚本语法,但建议保持简单,主要用于环境变量的设置。复杂的逻辑和交互式配置应该放在 ~/.zshrc 中。

项目环境变量

虽然我们通过 ~/.zshenv 文件实现了全局环境变量的配置,但很多时候,我们会有更加个性化的配置需求,比如不同的项目会使用同一环境变量的不同值。

这时,我们可以使用 direnv ⧉ 工具来管理项目的环境变量。

direnv 是一个强大的环境变量管理工具,它可以根据当前目录自动加载和卸载环境变量。当你进入包含 .envrc 文件的目录时,direnv 会自动加载该文件中定义的环境变量;当你离开目录时,这些环境变量会被自动卸载。

快速安装

direnv 支持跨平台,可以从 direnv 的官方网站获取安装脚本并执行:

curl -sfL https://direnv.net/install.sh | bash

在 macOS 上,也可以使用 Homebrew 快速安装 direnv:

brew install direnv

然后,需要在 ~/.zshrc 文件中添加以下内容:

eval "$(direnv hook zsh)"

如果你是按照我的 OhMyZsh 配置完全指南 来配置的 zsh,那么我们在配置中使用了官方自带的插件实现同样的效果。

此外,在 Cursor/VS Code 中,我们还可以安装 direnv for VSCode ⧉ 插件来优化下面章节中各种操作的体验。

基本使用

首先,在项目目录中创建 .envrc 文件:

export API_KEY="your-api-key"
export DATABASE_URL="postgresql://localhost:5432/mydb"

然后,再次进入项目目录后,direnv 会自动加载 .envrc 文件。

由于 .envrc 文件中可能包含敏感信息,direnv 默认会禁止加载。因此,会出现以下提示:

direnv: error .envrc is blocked. Run `direnv allow` to approve its content

执行 direnv allow 命令后,direnv 会验证文件内容的安全性后,加载文件中定义的环境变量。

在后续再次进入目录时,direnv 会自动检查并加载已允许的 .envrc 文件。

如果 .envrc 文件内容发生变化,direnv 会检测到文件内容的变化(哈希值改变),并阻止自动加载,显示安全提示,需要重新执行 direnv allow 命令。

当离开目录时,direnv 会自动卸载该目录的环境变量,就像从来没有加载过一样。

提示

这种基于哈希值的验证机制可以防止 .envrc 文件被恶意修改后自动加载,是 direnv 的重要安全特性。

设想一下,你从网上下载了一个项目,然后 .envrc 中有恶意代码,直接把你的系统给黑了。

有了 direnv 的这套强制性的机制,就可以有效避免这种情况的发生。

常用命令

  • direnv allow - 允许加载当前目录的 .envrc 文件
  • direnv deny - 禁止加载当前目录的 .envrc 文件
  • direnv reload - 重新加载当前目录的 .envrc 文件
  • direnv status - 显示当前 direnv 的状态

安全性考虑

  • .envrc 文件应该添加到 .gitignore 中,避免敏感信息泄露
  • 建议创建 .envrc.example 作为模板文件提交到代码仓库
  • 加载首次使用或者修改后的 .envrc 文件时需要手动允许,这是一个重要的安全特性

高级用法

direnv 同样支持所有的 shell 语法,因此可以实现更加复杂的环境变量管理。

这个项目的作者提供了一个 direnv stdlib ⧉ 库,里面自定义了一些和变量操作相关的常用指令,可以方便的实现各种复杂的环境变量管理需求。

最佳实践

为了保留最大的灵活性,建议按照以下层级结构来管理环境变量:

  1. 系统级环境变量

    • 使用 ~/.zshenv 文件
    • 适用于所有项目的通用设置
  2. 项目根目录

    • 使用 .envrc 文件
    • 设置项目的基础环境变量
    • 例如:项目的根路径、共享的 API 端点等
  3. 项目子目录

    • 使用 .envrc 文件 + source_up
    • 添加子目录特有的环境变量
    • 按需继承并覆盖父级设置

一个典型的项目,其环境变量文件结构如下:

1
2
3
4
5
6
~/.zshenv # 系统级环境变量
~/projects/
└── my-project/ # 项目根目录
├── .envrc # 项目基础配置
└── frontend/
└── .envrc # 前端特有配置

项目根目录中的 .envrc 内容如下:

export PROJECT_ROOT=$(pwd)
export API_URL="http://api.example.com"

子目录中 frontend/.envrc 内容如下:

1
2
3
source_up
export VITE_API_URL=$API_URL
export NODE_ENV="development"

这种层级结构可以:

  • 保持配置的清晰和模块化
  • 避免重复定义相同的变量
  • 方便管理不同环境的配置
  • 允许子目录根据需要覆盖父级设置

注意 source_up 命令出现的位置,会影响变量的覆盖顺序:

  • .envrc 文件开头使用,父目录的变量会先加载,然后被当前目录的设置覆盖。
source_up
export API_KEY="child-key"  # 会覆盖父目录中的 API_KEY
  • .envrc 文件末尾使用,当前目录的变量会先设置,然后可能被父目录的设置覆盖。
export API_KEY="child-key"
source_up  # 父目录中的 API_KEY 会覆盖上面的设置

因此,通常建议将 source_up 放在文件开头,这样可以:

  • 清晰地看到继承关系
  • 方便在当前目录中覆盖父目录的设置
  • 避免意外的变量覆盖