跨平台模块系统
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
调用,该调用仅在某些平台上可用。 (值得注意的是,如果不加载额外的模块,它不会出现在浏览器上。)我怀疑你把它放在那里作为稍后开发的功能的占位符,但我想我还是会提到它,因为跨平台支持是你的目标之一。
编辑:为了找出解决方案,我编辑了 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
andexports
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
调用,该调用仅在某些平台上可用。 (值得注意的是,如果不加载额外的模块,它不会出现在浏览器上。)我怀疑你把它放在那里作为稍后开发的功能的占位符,但我想我还是会提到它,因为跨平台支持是你的目标之一。