跨平台模块系统

时间:2019-02-19 07:27:52

标签: node.js module setter defineproperty

编辑:为了尝试找出解决方案,我编辑了该帖子以更清楚地说明我要完成的工作。

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

理想情况下,它应该可以在任何ES5运行时引擎上运行,但是主要目标是node.js和浏览器。


我要完成的工作是使用setter创建一个全局对象,所设置的对象就是模块的内容。 Node.js通过module.exports = {}完成了此操作,我正在尝试复制这种行为。

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


首次尝试:

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


一种改进的解决方案,但尚不完善:

我还尝试使用在导出的对象中定义的name属性来创建此映射,但是事实证明这种方法无效且容易规避。即通过导出不符合其用途的名称,并且可以有意或无意地覆盖系统中的其他模块。


以下是示例代码:

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


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

我也愿意接受任何其他建议来完成同一任务,而无需使用任何特定于节点的建议,因为我计划使该跨平台兼容。

1 个答案:

答案 0 :(得分:1)

  

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

您的问题是您确实在使用全局对象。由于模块是异步加载的,因此模块执行时全局对象可能处于错误状态。可能有一种方法可以在调用require之后重置全局对象,以使例子可以很好地工作,但是在某些情况下它并不能解决问题,而且您将长时间玩弄带有虫子的w鼠。

尽管module看起来像一个全局对象,但实际上它是为每个模块重新创建的对象。 documentation对此是明确的:

  

[Node.js]帮助提供一些实际上是模块特有的全局变量,例如:

     
      
  • 实现者可以用来从模块中导出值的moduleexports对象。
  •   
  • 便捷变量__filename__dirname,包含模块的绝对文件名和目录路径。
  •   

为模块提供独立的对象进行修改将使代码总体更加简单。

在我上面引用的文档部分中,您发现:

  

在执行模块的代码之前,Node.js将其包装为   函数包装,如下所示:

(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的使用注意事项。您可能听说过“评估是邪恶的”。没错,尽其所能。俗话在这里提醒人们const x = /* value from some user input */; eval('table.' + x );既不必要(因为您可以做table[x])又很危险,因为用户输入被评估为原始数据,并且您不信任用户输入任意运行代码。用户会将x设置为会做恶作剧的内容。在某些情况下(如此处的情况),仍然需要使用eval。在浏览器中,可以通过将源推送到eval并侦听script事件来避免load,但是在安全性方面却一无所获。再者,这是特定于平台的。如果您使用的是Node.js,则可以使用vm module,但是它带有以下免责声明:“ vm模块不是安全机制。请勿使用它运行不受信任的代码。”而且它也是特定于平台的。


顺便说一句,您当前的代码不是跨平台的。您的代码取决于require调用,该调用仅在某些平台上可用。 (在没有加载其他模块的情况下,它显然不存在于浏览器中。)我怀疑您认为它是稍后将要开发的功能的占位符,但我想还是要提到它,因为跨平台支持是您的目标之一。 / p>