toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

Node.js 环境变量 #338

Open toFrankie opened 4 months ago

toFrankie commented 4 months ago

配图源自 Freepik

前言

在 Node.js 中通常会使用 process.env 来获取环境变量。

process.env

它返回一个包含用户环境的对象。这里的用户环境是 Shell 进程,这个对象包含了当前进程的变量。注意,process.env 对象可以被修改,但其修改不会影响到此进程之外。

Shell 变量

分类:

作用域:

Shell 环境是天然隔离的,在当前进程内设置或修改变量,都不会影响到其他非关联进程的环境变量。

function fn() {
  foo=1         # 作用域为当前进程
  local bar=2   # 作用域为当前函数
  export baz=3  # 作用域为当前进程及子进程
}

fn

echo $foo    # 1
echo $bar    # 空字符串
echo $baz    # 3

在 Shell 中,如果引用的变量不存在,它不会报错,而是输出空字符。

NPM 环境变量

假设有以下包:

{
  "name": "node-env",
  "version": "1.0.0",
  "scripts": {
    "start": "node -e 'console.log(process.env)'"
  }
}

执行 npm run start 时,会得到这些变量:

{
  // Shell 内置变量
  SHELL: '/bin/zsh',
  USER: 'frankie',
  HOME: '/Users/frankie',
  // ...

  // zsh 自定义环境变量
  NVM_DIR: '/Users/frankie/.nvm',
  // ...

  // npm config 相关变量
  npm_config_sass_binary_site: 'https://npmmirror.com/mirrors/node-sass',
  npm_config_prefix: '/Users/frankie/.nvm/versions/node/v18.16.0',
  // ...

  // npm package 相关变量
  npm_package_json: '/Users/frankie/Web/Git/html-demo/src/demo/node-env/package.json',
  npm_package_name: 'node-env',
  npm_package_version: '1.0.0',
  // ...
}

process.env 的值都是字符串。如果赋值时不是字符串,会被隐式转换为字符串。

可以看到两类与 npm 相关的环境变量,在执行 npm run 命令时自动载入。

其中 npm_config_ 开头的环境变量源自 .npmrc 配置文件,优先级从上到下:

其中 key 大小写不敏感,它们都会被转换为小写形式,- 也会被转为 _

其中 npm_package_ 则源自 package.json。比如使用 process.env.npm_package_version 获取包版本号。

NPM Script 自定义环境变量

以上是 npm run 内部执行逻辑带入的环境变量,也可以自定义。

比如:

{
  "name": "node-env",
  "version": "1.0.0",
  "scripts": {
    "start": "NODE_ENV=development node -e 'console.log(process.env.NODE_ENV)'"
  }
}

这样就能在 Node 脚本里获取到这个 NODE_ENV 变量值了。

在命令前加上变量声明,它会传递给子进程。类似 export 的效果,但不完全相同,这种方式不会影响当前进程的同名变量。Simple Command Expansion

但它仅支持 Unix-like 操作系统,到 Windows 就不行了。后者需要使用 set 命令:

{
  "name": "node-env",
  "version": "1.0.0",
  "scripts": {
    "start": "set NODE_ENV=development node -e 'console.log(process.env.NODE_ENV)'"
  }
}

注意,Windows 操作系统的环境变量不区分大小写。

后来,出现了一些跨平台方案,比如 cross-env。用法变成了这样:

$ npm i cross-env -D
{
  "name": "node-env",
  "version": "1.0.0",
  "scripts": {
    "start": "cross-env NODE_ENV=development node -e 'console.log(process.env.NODE_ENV)'"
  }
}

cross-env is "finished" (now in maintenance mode)

如果项目的环境变量很多,script 就会很长很长,不好看也不好维护,后来又使用 dotenv 方案。

比如,项目根目录有 .env.env.development 文件:

由于 .env 文件可能会包含像密钥这类敏感信息,它不在版本控制范围内,应该添加到 .gitignore 里。如果是多人协作的项目,可以考虑添加类似 .env.example 模板到仓库里,以便其他成员清楚了解用到哪些环境变量。

# .env
API_URL=https://example.com/api/
# .env.development
API_URL=https://dev.example.com/api/
{
  "name": "node-env",
  "version": "1.0.0",
  "scripts": {
    "start": "cross-env NODE_ENV=development node -e 'console.log(process.env.API_URL)'",
    "build": "cross-env NODE_ENV=production node -e 'console.log(process.env.API_URL)'"
  }
}

这样本地开发和打包的时候,就能根据 NODE_ENV 的值从对应的 .env 文件中读取配置。

当然,以上环境变量仅可在编译时有效。要在业务代码中使用,还得借助类似 webpack.DefinePluginwebpack.EnvironmentPlugin 等插件处理,它们将会在编译时被替换为相应的字符串。

$ npm i dotenv
const webpack = require('webpack')
require('dotenv').config()

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
  ],
}

如果已有同名环境变量,dotenv 解析时将会忽略它。比如开发环境中先后加载 .env.development.env,其中解析前者时已设置 API_URL 变量,当解析到后者时就会忽略 API_URL

以上仅为示例,如果你是使用 webpack 的话,可以用 dotenv-webpack

Node.js 20.6.0 原生支持 .env 文件,处于实验性阶段,当前还有很多功能上的缺失,不能完全替代 dotenv。更多请看 Node.js 20.6.0 includes built-in support for .env files

注意,很多构建工具只有「特定前缀开头」以及像 NODE_ENV 这种很通用的环境变量才能在运行时(即业务代码)可用。

任何不能对外公开的信息,都不要嵌入构建当中,因为它们都可以在构建产物中查到。

其他

除此之外,还有其他一些方式可以提供。

webpack

可以通过 webpack-cli 的 --env 参数传递。比如:

$ npm i webpack webpack-cli -D
{
  "name": "node-env",
  "version": "1.0.0",
  "scripts": {
    "start": "webpack -w --env test",
    "build": "webpack --env prod",
    "build:pre": "webpack --env pre",
  }
}

执行 npm run build:pre 时,可以这样获取到值:

// webpack.config.js
module.exports = function (env, argv) {
  console.log(env.pre) // true
  // 可以结合 webpack.DefinePlugin 使用
  // ...
}

若使用 --env,webpack 配置需导出为函数。

更多请看 Environment Options

还想多说一下。

以 webpack 为例,其模式有 developmentproductionnone 三种。当「显式」声明 mode 为前两者时,它会自动设置 process.env.NODE_ENV 为对应值(更多)。从这个角度看,process.env.NODE_ENV 通常用来区分开发模式、打包模式。比如,开发模式下启用 sourcemap、HMR 等以便于开发调试。打包模式下启用 minimizer、splitChunks 等以减少产物体积。

但好像有些同学会将 process.env.NODE_ENV 用于区分「项目」的测试、生成环境,其实“不对”的。假设项目有测试环境、预生产环境和生产环境呢,那它就不够用了。而且,即使是部署到非正式环境,在打包时也应该使用 production 模式。

可以像上面那样不同项目环境传入不同的 --env 参数,然后结合 webpack.DefinePlugin 来定义特定变量,比如:

const webpack = require('webpack')

module.exports = function (env, argv) {
  return {
    // ...
    plugins: [
      new webpack.DefinePlugin({
        'process.env.TEST': env.test,
        'process.env.PRE': env.pre,
        'process.env.PROD': env.prod,
      }),
    ],
  }
}
// 业务
export const IS_TEST = process.env.TEST
export const IS_PRE = process.env.PRE
export const IS_PROD = process.env.PROD

export const API_URL = IS_TEST
  ? 'http://test.example.com/api/'
  : IS_PRE
  ? 'http://pre.example.com/api/'
  : 'http://example.com/api/'

说那么多,是为了不要混淆 --env--modeprocess.env.NODE_ENV 的关系。process.env.NODE_ENV 在各大构建工具频繁出现,算是一个约定俗成的变量了,它与项目环境是不同的概念。

未完待续...