跨平台模块系统

Cross platform module system

编辑:为了找出解决方案,我编辑了 post 以更清楚地解释我要完成的工作。

我正在尝试重新发明轮子,用最少的代码创建一个跨平台的异步模块加载系统

理想情况下,这应该适用于任何 ES5 运行时引擎,但主要目标是 node.js 和浏览器。


我想要完成的是创建一个带有 setter 的全局对象,从中设置的对象是模块内容。 Node.js 通过 module.exports = {} 实现了这一点,我正在尝试复制这种行为。

我遇到的问题很有趣,因为全局 setter 不会创建模块文件名和导出对象的 1:1 映射。


第一次尝试:

到目前为止,我已经尝试绑定 setter 以特定于特定函数调用。它总是求助于最后一个 module 被加载。我想通过将 setter 包装在闭包中,它会将 module 参数保留在调用堆栈中,但我错了 - 因为 setter 发生了变化。


一个改进的解决方案,但还不够完善:

我也尝试过使用导出对象中定义的name 属性来创建这个映射,但已被证明是无效的并且很容易绕过。 IE。通过导出一个与其作用不符的名称,并且可以有意或无意地覆盖系统中的其他模块。


下面是一些示例代码:

let exporter = {}
global.exporter = exporter

const imports = function(module, callback) {
  return new (function(module, callback) {
    Object.defineProperty(exporter, 'exports', {
      enumerable: false,
      configurable: true,
      set: function(exportFile) {
        console.log('Setting export file:', exportFile.name, ':', module)
        callback(exportFile)
      },
    })

    console.log('loading module: ', module)
    require(module)
  })(module, callback)
}

在模块文件中使用setter:

exporter.exports = {
  name: 'File1',
}

使用新导入的示例代码。

function load(name) {
  imports(__dirname + '/modules/' + name, function(exportFile) {
    console.log('Module loaded: ', exportFile.name)
  })
}

load('1') // instant
load('2') // 2 second timeout
load('3') // 1 second timeout

输出:

loading module:  .../modules/1
Setting export file: File1 : .../modules/1
Module loaded:  File1
loading module:  .../modules/2
loading module:  .../modules/3
Setting export file: File3 : .../modules/3
Module loaded:  File3
Setting export file: File2 : .../modules/3
Module loaded:  File2


我感谢任何可以解决此上下文问题的帮助!

我也愿意接受任何其他建议来完成同样的任务,而不使用任何节点特定的东西,因为我计划使这个跨平台兼容。

What I'm trying to accomplish is creating a global object with a setter, from which the object being set is the module contents. Node.js accomplishes this with module.exports = {} and I'm trying to replicate this behavior.

你的问题是你确实使用了一个全局对象。由于模块是异步加载的,因此当模块执行时,全局对象可能处于不正确的状态。 在您的 require 调用之后可能有一种方法可以重置全局对象,以便 你的具体例子 工作正常,但有些情况它不会涵盖,你将在很长一段时间内与错误一起玩打地鼠。

虽然module看起来像一个全局对象,但实际上它是为每个模块重新创建的对象。 documentation 明确说明了这一点:

[Node.js] helps to provide some global-looking variables that are actually specific to the module, such as:

  • The module and exports objects that the implementor can use to export values from the module.
  • The convenience variables __filename and __dirname, containing the module's absolute filename and directory path.

为您的模块提供要修改的独立对象将使代码更简单整体

在我上面引用的文档部分上面,你会发现:

Before a module's code is executed, Node.js will wrap it with a function wrapper that looks like the following:

(function(exports, require, module, __filename, __dirname) { 
// Module code actually lives in here 
});

您可以从中提取一个页面并使用类似这样的包装器:

(function (exporter, ...) {
// Module code here...
});

这是一个例子:

    const source = `
    exporter.exports = {
      SomeVar: "Some Value",
    };
    `;

    function wrapInFunction(source) {
      return `(function (exporter) { ${source} })`;
    }

    const exporter = {
      exports: {},
    };

    eval(wrapInFunction(source))(exporter);
    console.log(exporter);

这里有一个关于 eval 用法的说明。您可能听说过 "eval is evil"。的确如此,就目前而言。这句话提醒人们 const x = /* value from some user input */; eval('table.' + x ); 是不必要的(因为你可以做 table[x])而且很危险,因为用户输入是原始评估的,你不相信 的用户输入运行任意码。用户会将 x 设置为做坏事的东西。在某些情况下,使用 eval 仍然是必要的,就像这里的情况一样。在浏览器中,您可以通过将源代码推入 script 并监听 load 事件来避免 eval,但您在安全方面没有任何收获。然后又是特定于平台的。如果您在 Node.js 中,您可以使用 vm module,但它附带此免责声明“vm 模块不是安全机制。请勿将其用于 运行不受信任的代码。”而且它也是特定于平台的。


顺便说一下,您当前的代码不是跨平台的。您的代码取决于 require 调用,该调用仅在某些平台上可用。 (值得注意的是,如果不加载额外的模块,它不会出现在浏览器上。)我怀疑你把它放在那里作为稍后开发的功能的占位符,但我想我还是会提到它,因为跨平台支持是你的目标之一。