ES6模块输入点的一致命令行执行
#javascript #node #es6 #sfjm

今天较短的帖子,带有一个适度的片段,我拼凑在一起,我怀疑我会从现在开始重复很多。

ES6模块很棒。您应该更频繁地使用它们。他们最终为我们提供了一种始终如一地跨命令行(例如Nodejs)和浏览器端上下文编写可重复使用的代码元素的方法。但这并非没有它的怪癖和抓。

SFJM ES6中的入口点

当我们尝试编写包含“入口点”的ES6模块时,出现了一个特定的问题。考虑这里采用的SFJM方法:

https://dev.to/tythos/single-file-javascript-modules-7aj

这可以轻松地使用诸如“ export default object.assign({exports},{metadata})之类的东西扩展到ES6实现”,这反过来又引起了问题:如果元数据包括__main__属性,则如何如何当从命令行(使用nodejs)调用脚本时,我们确保将其始终调用,但是在其他上下文中加载时不是?当然,我在想类似Python的入口点行为:

def main():
    pass

if __name__ == "__main__":
    main()
例如,在SFJM的情况下,这可能看起来像:

const exports = Object.assign({
    "myfunction": (a) => { console.log(`the square of ${a} is ${a*a}`) }
}, {
    "__main__": () => exports.myfunction(Math.PI)
});

export default exports;

您会在上述情况下注意到我们将常数exports对象的定义与export default“ return”分开。我们这样做的原因有两个:

  1. __main__行为可以参考特定导出符号

  2. 本文中的其余内容可以集中于定义exports后的特定“入口点”处理程序行为

模块上下文

首先,让我们考虑一下可能加载模块的上下文。此用例有四个特定上下文。

  1. 可以在浏览器上下文中导入该模块。显然,我们希望出口行为保持一致,但我们不希望调用__main__行为。

  2. 该模块可以通过repl加载在nodejs上下文中。 NODEJS对ES6模块的支持不再是实验性的,但确实带有一些警告 - 例如,您需要使用动态导入,并且并非所有上下文资源都可以使用。

  3. 该模块可以通过其他一些模块(例如,下游依赖关系)加载在Nodejs上下文中。像以前的上下文一样,我们不希望调用__main__行为,但是我们确实需要在没有错误的情况下透明地处理案例。

  4. 最后,我们可以通过直接将其传递到node可执行文件来调用该模块作为脚本。这是唯一应调用__main__行为的情况。

Nodejs ES6警告

如果您尝试在nodejs中定义或调用ES6模块,您会注意到一些事情。

第一个也是最明显的是Nodejs希望您使用“ .mjs”文件扩展名。否则,您需要在包装文件中定义"type": "module"属性。因此,“ .mjs”是。

第二,更微妙的是,您习惯于在Nodejs模块中拥有的许多资源可能存在或可能不存在。稍后的一个相关示例是querystring符号,您需要明确导入。但是您需要动态执行此操作,因为Nodejs将您评估为ES6模块,因此require()调用被拒绝!

最后,我们需要考虑我们需要参考的值需要参考以确定我们处于命令行调用中(例如,上面的情况#4),而其他案例则没有。通常,从nodejs模块中,我们可以检查module.id并将其与"."进行比较(与Python的__name__ == "__main__"行为相当)。但是module符号(非常类似于exportsrequire符号)在nodejs es6上下文中不存在!

我们需要的

相反,我们需要检查两个值:

  • process.argv.length(谢天谢地,process符号仍然存在),这将有效地断言我们在nodejs上下文(而不是浏览器上下文)

  • import.meta.url(感谢import.meta.url),这将有效地断言我们在ES6模块上下文

到目前为止,一切都很好。我们将要检查这些值是否指向相同的路径(特别是我们的模块的路径)。但是,如果您打印它们,您会注意到略有不同的值:

  • process.argv[1]将具有“ c:\ users \ my user \ testmod.mjs”之类的东西。

  • import.meta.url将具有“ file:/// c:/users/my%20user/testmod.mjs”

  • 之类的东西。

所以,我们需要在比较之前“按摩”:

  1. 对于process.argv[1],我们将用“/”替换“ \”,以确保我们支持Windows和 *NIX操作系统;我们还需要预先准备一个“ file:///”字符串

  2. 对于import.meta.url,我们要取消示例任何空间或其他HTTP风格的路径编码;通常,我们将使用querystring符号,但是它在Nodejs ES6上下文中不可用,因此我们需要导入它。但是我们不能导入“正常”的方式(例如,require()import stuff from "stuff"),因为nodejs要求我们使用动态导入。因此,闭合。

终于在那里!

最终结果看起来像这样:

const exports = Object.assign({
    "myfunction": (a) => { console.log(`the square of ${a} is ${a*a}`) }
}, {
    "__main__": () => exports.myfunction(Math.PI)
});

if (process.argv.length > 1 && import.meta) {
    import("querystring").then(querystring => {
        if (`file:///${process.argv[1].replace(/\\/g, "/")}` === querystring.unescape(import.meta.url)) {
            exports.__main__();
        }
    });
}

export default exports;

测试

让我们尝试一下!首先,从命令行调用:

$ node testmod.mjs
the square of 3.141592653589793 is 9.869604401089358

然后,从nodejs depp上下文:

$ node
> import("testmod.mjs").then(console.log)
...
[Module: null prototype] {
  default: {
    myfunction: [Function: myfunction],
    __main__: [Function: __main__]
  }
}

嘿,还不错!如果需要,您甚至可以在“动态导入之后”中从补充中调用入口点,但是(谢谢,nodejs!)您需要从default符号中专门提取它:

> import("../sfjm/testmod.mjs").then(testmod => testmod.default.__main__())
...
> the square of 3.141592653589793 is 9.869604401089358

这也应抵抗依赖性进口和浏览器上下文。它不是超级简洁的,但是足够短,并且足够通用,可以复制到定义__main__导出的任何SFJM模块的末端。享受!