如何正确使用环境变量

作者:斯坦利·阮

环境变量是应用程序开发人员的基本概念之一。它们是我们每天使用的东西。

环境变量甚至在事实上的十二因子应用程序了一部分。它们有很多好处,其中包括应用程序可配置性和安全性,在许多资源中,如资源,甚至是StackOverflow的资源,都涵盖了这些内容。

环境变量很棒,我完全支持这个想法。但是,一切都是有代价的–如果不慎使用环境变量,可能会对我们的代码库和应用程序产生有害影响。

环境变量的诅咒

如果环境变量可以帮助我们编写更安全的代码并更轻松地针对不同的环境配置应用程序,那么环境变量将如何成为坏事?

有趣的是,环境变量的缺点实际上源于它们的本质,这使其变得非常出色:它们是全局的和外部的,应用程序开发人员可以通过它们注入配置并在更难以妥协的地方管理这些秘密。

作为开发人员,我们都知道应用程序的全局状态有多糟糕。而且请不要相信我的话,在这里这里这里的很多地方都讨论了这些弊端。

在本文中,我将重点介绍处理环境变量时最常遇到的2个主要缺陷:

  • 僵化/可测试性差
  • 代码理解/可读性

如何正确使用环境变量

与我如何处理应用于错误位置的全局变量或全局模式(例如单例)类似,我最喜欢的武器是依赖注入

我们不会对依赖关系进行编码完全相同,但是原理是相同的。与其直接使用环境变量(依赖项),不如将它们注入callsite(即实际使用的位置)。这将关系从“依赖的呼叫站点”转换为“需要的呼叫站点”。

依赖注入通过以下方式解决了这些问题:

  • 允许开发人员在测试时更轻松地注入配置
  • 将代码阅读者的思维范围缩小到仅用于软件包,消除了所有外部性

那么我们如何应用这些原则?

我将使用一个Node.js示例来演示如何重构代码库并消除分散的环境变量。

假设情况

假设我们有一个带有单个端点的简单应用程序,它将查询PostGres数据库中的所有TODO。这是我们的数据库模块,其中散布了环境变量:

const { Client } = require("pg");

function Postgres() {
  const c = new Client({
    connectionString: process.env.POSTGRES_CONNECTION_URL,
  });
  this.client = c;
  return this;
}

Postgres.prototype.init = async function () {
  await c.connect();
  return this;
};

Postgres.prototype.getTodos = async function () {
  return this.client.query("SELECT * FROM todos");
};

module.exports = Postgres;

并且此模块将通过应用程序的入口点注入到我们的HTTP控制器中:

const express = require("express");
const TodoController = require("./controller/todo");
const Postgres = require("./pg");

const app = express();

(async function () {
  const db = new Postgres();
  await db.init();
  const controller = new TodoController(db);
  controller.install(app);

  app.listen(process.env.PORT, (err) => {
    if (err) console.error(err);
    console.log(`UP AND RUNNING @ ${process.env.PORT}`);
  });
})();

浏览上面的入口点文件,我们无法得知应用程序对环境变量(或通常的环境配置)的要求是什么(代码扫视度min减点)。

重构代码

改进先前布置的代码的第一步是确定直接使用环境变量的所有位置。

对于上面的特定情况,由于代码库很小,因此非常简单。但是对于较大的代码库,您可以使用lint之类的整理工具来直接扫描所有使用环境变量的位置。刚刚成立了一个规则,例如,禁止环境变量(如node/no-process-enveslint-插件节点)。

现在是时候从我们的应用程序模块中删除对环境变量的直接使用,并将这些配置作为模块要求的一部分包括在内:

...
function Postgres(opts) {
  const { connectionString } = opts;
  const c = new Client({
    connectionString,
  });
  this.client = c;
  return this;
}
...

这些配置将仅从我们应用程序的入口点提供:

...
const db = new Postgres({
  connectionString: process.env.POSTGRES_CONNECTION_URL,
});
...

从入口点开始,我们的应用程序现在对环境的要求要清楚得多。这样可以避免遗忘要添加的环境变量带来的潜在问题。

上面演示的完整源代码可以在这里找到。

奖励:常见问题

这些是我认为可能会由阅读本文的人提出的一些问题。也许它们并不是实际的常见问题,但嘿,提出可能的替代观点有何害处?

为什么不使用中央配置文件/模块?

我已经看到了很多尝试使用中央位置来绘制这些值(例如,config.js用于Node项目的文件/模块)来解决此问题的尝试。

但是,这种方法并不比实际使用应用程序运行时提供的环境变量更好(例如process.env),因为所有内容仍在某种程度上处于全局状态(整个应用程序使用单个配置对象)。

实际上,情况可能更糟,因为现在我们为代码引入了另一个位置。

如果我想为模块进行零配置设置怎么办?

是的,谁不喜欢零配置,随时可用的模块。再一次,我想重申一下,构建软件就是要权衡取舍,这是以整个文章一直在讨论的可读性为代价的。

如果您仍然希望进行零配置设置,我建议您使用config对象(即opts前面的代码示例中的Constructor参数)并将直接使用环境变量作为回退,如下所示:

function Postgres(opts) {
  const connectionString =
    opts.connectionString || process.env.POSTGRES_CONNECTION_URL;
  const c = new Client({
    connectionString,
  });
  this.client = c;
  return this;
}

这样,我们代码的读者仍将能够识别模块的需求(尽管扫视性较低,因为它已经被换成零配置性)。

阅读原文

本文来自投稿,不代表微擎百科立场,如若转载,请注明出处:https://www.w7.wiki/develop/4621.html

发表评论

登录后才能评论