编辑:为了尝试找出解决方案,我编辑了该帖子以更清楚地说明我要完成的工作。
我正在尝试以最少的代码重新发明轮子,以创建跨平台异步模块加载系统
理想情况下,它应该可以在任何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
感谢您可以解决此上下文问题的任何帮助!
我也愿意接受任何其他建议来完成同一任务,而无需使用任何特定于节点的建议,因为我计划使该跨平台兼容。
答案 0 :(得分:1)
我要完成的工作是创建一个带有setter的全局对象,从中设置的对象就是模块的内容。 Node.js通过
module.exports = {}
完成了此操作,我正在尝试复制这种行为。
您的问题是您确实在使用全局对象。由于模块是异步加载的,因此模块执行时全局对象可能处于错误状态。可能有一种方法可以在调用require
之后重置全局对象,以使例子可以很好地工作,但是在某些情况下它并不能解决问题,而且您将长时间玩弄带有虫子的w鼠。
尽管module
看起来像一个全局对象,但实际上它是为每个模块重新创建的对象。 documentation对此是明确的:
[Node.js]帮助提供一些实际上是模块特有的全局变量,例如:
- 实现者可以用来从模块中导出值的
module
和exports
对象。- 便捷变量
__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>