如何有条件地修改 ES6 Javascript 模块的导出

How to conditionally modify exports from an ES6 Javascript module

提问人:Morgan 提问时间:11/11/2023 更新时间:11/12/2023 访问量:69

问:

我想知道是否有可能以某种方式仍然从模块内访问 ES6 模块的模块导出,就像您在 CommonJS 中使用 module.exports 所做的那样。

为了清楚起见,我有一个js模块(Config.js),我用它来导出我所有的配置变量。

export const DatabaseName = "myDbName";
export const DatabasePort = 3000;
export const DatabaseHosts = ["174.292.292.32"];
export const MaxWebRequest = 50;
export const MaxImageRequests = 50;
export const WebRequestTimeout = 30;
etc...

然后我有一个单独的Dev.Config.js文件,它只包含我的开发环境的覆盖。

export const DatabaseHosts = ["localhost"];
export const DatabasePort = 5500;

在我的主 Config.js 文件中,我的底部有这个逻辑。

try {
    var environmentConfig = `./${process.env.NODE_ENV}.Config.js`;
    var localConfig = require(environmentConfig)
    module.exports = Object.assign(module.exports, localConfig)
} catch (error) {
    console.log("Error overriding config with local values. " + error)
}

最后,在我的消费代码中,我可以像这样导入我的 config.js 文件

import * as Config from "./Config.js";

console.log(Config.DatabaseHosts) // Gives me the correct "overridden" value on my dev environment

目前,我一直在使用 babel 将我的代码全部转译回 CommonJS,我想这就是我能够混合和匹配导入/导出语法的方式,并且仍然像我上面所做的那样引用 module.exports。

我的问题是,我如何在纯 ES6 模块中复制此模式,而无需使用 babel 进行转译,因为我无法从模块本身中修改我的 module.exports?

JavaScript 节点.js babeljs es6-modules

评论

0赞 Dave Newton 11/11/2023
不使用现有的配置解决方案是否重要?
0赞 Morgan 11/11/2023
如?这似乎是一段微不足道的代码,我没有考虑使用其他配置解决方案。但是,如果有一个常用的选项,我愿意接受。仍然很好奇,但从纯粹的学术角度来看,上述模式如何在纯 ES6 中复制。
0赞 morganney 11/12/2023
如果您的节点版本支持,只需使用动态导入和顶级等待即可。其余的都是黑客。从主配置模块中导出 promise。相信我,我们有相同的名字;)
0赞 Dave Newton 11/12/2023
@Morgan 建议是 OT,但规范的建议是 .还有其他的(dotenv 还可以,cpl 其他的相似但不同,值得一看)。dotenv

答:

2赞 jsejcksn 11/12/2023 #1

ESM 不支持条件导出模式。

为了使用来自另一个模块的动态导入来修改导出的值,该模块的说明符派生自环境变量(在尝试...catch 语句,以便失败的尝试不会在顶层抛出未捕获的异常),您可以修改导出的结构,以便将它们公开为对象上的属性。下面是一个可重现的例子来演示:

./package.json:

{
  "name": "so-77465699",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "NODE_ENV=Dev node main.js",
    "prod": "NODE_ENV=production node main.js"
  },
  "license": "MIT"
}

./Dev.Config.js:

export const Config = {
  DatabaseHosts: ["localhost"],
  DatabasePort: 5500,
};

export default Config;

./Config.js:

const Config = {
  DatabaseName: "myDbName",
  DatabasePort: 3000,
  DatabaseHosts: ["174.292.292.32"],
  MaxWebRequest: 50,
  MaxImageRequests: 50,
  WebRequestTimeout: 30,
};

try {
  // Import a module specifier based on
  // the value of the NODE_ENV environemnt variable.
  // Destructure and rename the default export:
  const { default: envConfig } = await import(
    import.meta.resolve(`./${process.env.NODE_ENV}.Config.js`)
  );

  // Iterate the keys and values, updating the existing Config object:
  for (const [key, value] of Object.entries(envConfig)) {
    Config[key] = value;
  }
} catch (cause) {
  console.log(`Error overriding config with local values: ${cause}`);
}

export { Config, Config as default };

./main.js:

// Import named export:
import { Config } from "./Config.js";

// Alternatively, since it's also the default export:
// import { default as Config } from "./Config.js";

// Or, using syntax sugar:
// import Config from "./Config.js";

console.log(Config.DatabaseHosts);

在终端中:

% node --version
v20.9.0

% npm run dev

> [email protected] dev
> NODE_ENV=Dev node main.js

[ 'localhost' ]

% npm run prod

> [email protected] prod
> NODE_ENV=production node main.js

Error overriding config with local values: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/node/so-77465699/production.Config.js' imported from /Users/node/so-77465699/Config.js
[ '174.292.292.32' ]

评论

0赞 morganney 11/12/2023
为什么不将 try/catch 包装在异步设置函数中,然后呢?我的意思是为什么不使用顶级等待?否则,在读取配置值时看起来像争用条件。export default await setup()
0赞 jsejcksn 11/12/2023
^@morganney 我不明白你的问题......使用顶级 await。将现有代码包装在函数中的目的是什么——只是为了立即调用它并导出等待的返回值?
0赞 morganney 11/12/2023
它在哪里使用?是否确定。。。您正在导出一个 pojo。
0赞 jsejcksn 11/12/2023
^@morganney 是的 — 它用于动态 .tryimport()
0赞 morganney 11/12/2023
当然,您正在进行异步调用作为导入的副作用,但这会让消费者等待承诺完成才能读取值吗?
0赞 Bergi 11/12/2023 #2

在 ES6 模块中无法动态构建/覆盖导出对象。即使是有条件的导入也已经需要顶级或工具支持。await

但是,您可以做的是弄乱声明和:leteval

export const DatabaseName = "myDbName"; // keep things as `const` to prevent overriding them
export let DatabasePort = 3000;
export let DatabaseHosts = ["174.292.292.32"];
export let MaxWebRequest = 50;
export let MaxImageRequests = 50;
export const WebRequestTimeout = 30;
… // etc

try {
    var environmentConfig = `./${process.env.NODE_ENV}.Config.js`;
    var localConfig = await import(environmentConfig)
    for (const [name, value] of Object.entries(localConfig)) {
        eval(`${name} = value;`);
    }
} catch (error) {
    console.log("Error overriding config with local values. " + error)
}

不过我不推荐这个。仅当无法更改 Config.js 模块的使用方式,或者导出的变量太多而替代方法不可行时,才使用此方法。

相反,我建议您创建一个单独的模块来合并配置,尽管这需要拼写两次配置名称:

// Default.Config.js
export const DatabaseName = "myDbName";
export const DatabasePort = 3000;
export const DatabaseHosts = ["174.292.292.32"];
export const MaxWebRequest = 50;
export const MaxImageRequests = 50;
export const WebRequestTimeout = 30;
// Dev.Config.js
export const DatabaseHosts = ["localhost"];
export const DatabasePort = 5500;
// Config.js
import * as defaultConfig from "./Default.Config.js";

const localConfig = await import(`./${process.env.NODE_ENV}.Config.js`).catch(error => {
    console.log("Error overriding config with local values. " + error);
    return {};
});

export const {
    DatabaseName,
    DatabasePort,
    DatabaseHosts,
    MaxWebRequest,
    MaxImageRequests,
    WebRequestTimeout,
    … // etc
} = Object.assign({}, defaultConfig, localConfig);

或者将配置模块更改为默认导出对象,您可以任意操作这些对象。但是,您失去了使用命名导入和获得静态验证的能力。

评论

0赞 jsejcksn 11/12/2023
如果对象中的任何键不是有效的标识符,则计算的字符串将不会生成有效的 JavaScript(并且会抛出)。
0赞 Bergi 11/12/2023
@jsejcksn 是的,但是如果所有导出都用 定义,它们将是有效的标识符,所以我认为这种风险是可以接受的(无论如何都会发现异常)。但我同意这不是一个很好的解决方案,并且已经编辑了答案以提供替代方案。export const …