詳解 dotenv 的使用與實現

一秋知叶發表於2024-08-27

每當涉及到保護API金鑰或我們不想因為開源專案而向公眾展示的東西時,我們總是傾向於.env檔案,而它的解析依賴到dotenv包,一個每週都有31k+開發人員下載的軟體包。其設計的理念是Twelve-Factor App的第三點。配置與程式碼分離。

關於Twelve-Factor App大家可以前往這裡檢視:https://12factor.net/

為什麼檔名只有.env?

檔名只能以.env開頭,是一個誤解。使用任何名字,它仍然可以在node.js上正常工作。

那為什麼要用點開頭?

當涉及到環境檔案時,在檔名前面使用一個點(.)被認為是好的,因為在任何檔名前面新增一個點都會使其成為一個隱藏的檔案或資料夾。

這就是為什麼您的作業系統中有多個資料夾,這些資料夾是隱藏的,只能透過CLI訪問,例如.ssh、.github、.vscode等。

使用介紹

首先,在你的 Node.js 專案中安裝 dotenv

npm install dotenv

在專案的根目錄中建立一個名為 .env 的檔案。這個檔案中每一行定義一個環境變數,格式為 KEY=VALUE。例如:

# .env 檔案
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
SECRET_KEY=mysecretkey

注意:.env 檔案通常不會提交到版本控制系統(如 Git),因此你可以在 .gitignore 檔案中新增一行 /.env 來忽略它。

在你的應用程式入口檔案中(通常是 app.jsindex.js),載入並配置 dotenv。這通常是你在應用程式中最早執行的操作之一:

require('dotenv').config();

// 現在你可以透過 process.env 訪問環境變數
console.log(process.env.PORT); // 輸出: 3000
console.log(process.env.DB_HOST); // 輸出: localhost

在一些情況下,你可能需要為不同的環境(如開發、測試、生產)使用不同的 .env 檔案。你可以透過 dotenv 的配置選項來指定載入的檔案:

require('dotenv').config({ path: './config/.env.dev' });

如果想讓配置支援使用變數替換,你可以使用 dotenv-expand 來實現:

# .env 檔案
HOST=localhost
PORT=3000
FULL_URL=http://${HOST}:${PORT}
const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');

const myEnv = dotenv.config();
dotenvExpand.expand(myEnv);

console.log(process.env.FULL_URL); // 輸出: http://localhost:3000

原始碼實現

dotenv的原始碼很簡單,只有1個主要檔案:https://github.com/motdotla/dotenv/blob/master/lib/main.js

鍵值解析

核心原理是將 .env 檔案解析為鍵值對,並載入到 process.env 中。在實現上主要是透過使用正規表示式,並處理了字串、引號、換行符等特殊情況。

這個正則,表示看著頭疼。。。

const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg

加解密

在16.1.x版本之後,還增加了解密的功能。可以用於一些安全要求較高的專案中。有了這個可以放心地把.env檔案提交到生產了。

function decrypt (encrypted, keyStr) {
  const key = Buffer.from(keyStr.slice(-64), 'hex')
  let ciphertext = Buffer.from(encrypted, 'base64')

  const nonce = ciphertext.subarray(0, 12)
  const authTag = ciphertext.subarray(-16)
  ciphertext = ciphertext.subarray(12, -16)

  try {
    const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
    aesgcm.setAuthTag(authTag)
    return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
  } catch (error) {
    // 處理解密錯誤
    throw error
  }
}

該函式使用 AES-256-GCM 演算法進行解密操作。AES-256 表示它使用 256 位的金鑰進行加密,GCM 是一種加密模式,除了加密資料,還可以驗證資料的完整性。

  • 加密: 先用 256 位的金鑰加密資訊,然後生成一個“標籤”,這個標籤是用來驗證資料有沒有被改動的。
  • 解密: 先檢查標籤,如果標籤正確,才會用金鑰解密資料。如果標籤不對,就說明資料可能被篡改了,解密就會失敗。

你會不會有疑問,這裡是解密,那加密呢?dotenv自身不提供加密的功能,加密依賴於一個工具,dotenvx。https://dotenvx.com/docs/。

dotenvx 是 dotenv 的擴充套件或增強版,通常基於 dotenv 的功能進行構建,使用時也會依賴於 dotenv 的基礎設施。dotenvx 提供了更加專業和複雜的功能,適用於更高要求的應用場景。

另外也可以透過dotenvx ext genexample命令生成一個env的配置例子檔案。

靈活的配置入口

dotenv 支援靈活設定配置檔案地址,這主要依賴於configDotenv 函式。同時,還內建了除錯功能,透過 _debug 函式輸出詳細的除錯資訊,幫助開發者快速定位問題。

function configDotenv (options) {
  const dotenvPath = path.resolve(process.cwd(), '.env')
  let encoding = 'utf8'
  const debug = Boolean(options && options.debug)

  if (options && options.encoding) {
    encoding = options.encoding
  }

  // 載入並解析 .env 檔案
  let optionPaths = [dotenvPath]
  if (options && options.path) {
    // 自定義路徑處理邏輯
  }

  // 解析並填充環境變數
  let lastError
  const parsedAll = {}
  for (const path of optionPaths) {
    try {
      const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))
      DotenvModule.populate(parsedAll, parsed, options)
    } catch (e) {
      if (debug) {
        _debug(`Failed to load ${path} ${e.message}`)
      }
      lastError = e
    }
  }

  // 填充到 process.env
  DotenvModule.populate(processEnv, parsedAll, options)

  return { parsed: parsedAll, error: lastError }
}

總結

關於dotenvx,這裡說多一點,真是一個好工具。除了上面介紹的用來加密,也可以用來生成配置用例

dotenvx ext genexample

也可以用來設定環境檔案,不用在專案裡自己呼叫dotenv也可以

dotenvx run -f .env.production -- node index.js

最後,提一下注意事項:

  • 非加密的env配置檔案,不要提交到程式碼倉庫。除非你確信其中不包含任何敏感資訊。
  • 分環境管理:為不同環境建立 .env 檔案,例如 .env.development, .env.production
  • 確保 .env 檔案的許可權設定是適當的,以防止未經授權的訪問。

本文由mdnice多平臺釋出

相關文章