具有 NodeJS/CommonJS 样式模块系统的语言
Languages with a NodeJS/CommonJS style module system
我真的很喜欢 NodeJS(以及它的浏览器端对应物)处理模块的方式:
var $ = require('jquery');
var config = require('./config.json');
module.exports = function(){};
module.exports = {...}
实际上我对 ES2015 'import' spec 非常失望,它与大多数语言非常相似。
出于好奇,我决定寻找其他实现甚至支持类似 export/import 风格的语言,但无济于事。
也许我遗漏了一些东西,或者更可能的是,我的 Google Foo 还没有达到标准,但如果能看看其他哪些语言以类似的方式工作,那将非常有趣。
有没有人遇到过类似的系统?
或者也许有人甚至可以提供它不经常使用的原因。
要正确比较这些特征几乎是不可能的。只能比较它们在特定语言中的实现。我主要通过语言 Java 和 nodejs.
收集我的经验
我观察到这些差异:
- 您可以使用
require
而不仅仅是让其他模块对您的模块可用。例如,您可以使用它来解析 JSON 文件。
- 您可以在代码中的任何地方使用
require
,而 import
仅在文件顶部可用。
require
实际上执行所需的模块(如果尚未执行),而 import
具有更多的声明性。这可能不适用于所有语言,但这是一种趋势。
require
可以从子目录加载私有依赖项,而 import
通常对所有代码使用一个全局命名空间。同样,这在一般情况下也不是真的,而只是一种趋势。
职责
如您所见,require
方法具有多重职责:声明模块依赖关系和读取数据。这最好与导入方法分开,因为 import
应该只处理模块依赖性。我想,您喜欢使用 require
方法来读取 JSON 的原因在于,它为程序员提供了一个非常简单的界面。我同意拥有这种简单的 JSON 阅读界面是件好事,但是没有必要将它与模块依赖机制混合在一起。可以有另一种方法,例如readJson()
。这将分离关注点,因此 require
方法仅在声明模块依赖项时才需要。
代码中的位置
现在,我们只使用 require
作为模块依赖项,在模块顶部以外的任何地方使用它是一种不好的做法。当您在代码中的任何地方使用它时,它只会让您很难看到模块依赖关系。这就是为什么您只能在代码之上使用 import
语句的原因。
我没有看到 import 创建全局变量的地方。它只是为每个依赖项创建一个一致的标识符,该标识符仅限于当前文件。正如我上面所说,我建议通过仅在文件顶部使用 require
方法来执行相同的操作。它确实有助于提高代码的可读性。
工作原理
加载模块时执行代码也可能是个问题,尤其是在大型程序中。您可能 运行 进入一个循环,其中一个模块可传递地需要自身。这真的很难解决。据我所知,nodejs 是这样处理这种情况的:当 A 需要 B 并且 B 需要 A 而你开始需要 A 时,那么:
- 模块系统记住它当前正在加载 A
- 执行A
中的代码
- 它记得当前正在加载 B
- 执行B
中的代码
- 它尝试加载 A,但 A 已经在加载
- A 尚未完成加载
- 它returns半载A到B
- B不希望A半载
这可能是个问题。现在,有人可以争辩说循环依赖确实应该避免,我同意这一点。但是,循环依赖只应避免在程序的独立组件之间。 类 在一个组件中经常有循环依赖。现在,模块系统可用于两个抽象层:类 和组件。这可能是个问题。
接下来,require
方法经常导致单例模块,不能在同一个程序中多次使用,因为它们存储全局状态。然而,这并不是系统的错,而是程序员以错误的方式使用系统的错。尽管如此,我的观察是 require
方法会误导新程序员这样做。
依赖管理
支持不同方法的依赖管理确实是一个有趣的点。例如 Java 在当前版本中仍然缺少合适的模块系统。再次宣布下一个版本,但谁知道这是否会成为现实。目前,您只能使用 OSGi 获取模块,这远非易用。
nodejs底层的依赖管理很强大。然而,它也不完美。例如,非私有依赖项,即通过模块 API 公开的依赖项,始终是一个问题。不过这个是依赖管理的通病所以不限于nodejs
结论
我想两者都没有那么糟糕,因为每个都被成功使用了。但是,在我看来,import
比 require
有一些 objective 优势,比如职责分离。由此可见 import
可以限制在代码的顶部,这意味着只有一个地方可以搜索模块依赖项。此外,import
可能更适合编译语言,因为这些语言不需要执行代码来加载代码。
我真的很喜欢 NodeJS(以及它的浏览器端对应物)处理模块的方式:
var $ = require('jquery');
var config = require('./config.json');
module.exports = function(){};
module.exports = {...}
实际上我对 ES2015 'import' spec 非常失望,它与大多数语言非常相似。
出于好奇,我决定寻找其他实现甚至支持类似 export/import 风格的语言,但无济于事。
也许我遗漏了一些东西,或者更可能的是,我的 Google Foo 还没有达到标准,但如果能看看其他哪些语言以类似的方式工作,那将非常有趣。
有没有人遇到过类似的系统? 或者也许有人甚至可以提供它不经常使用的原因。
要正确比较这些特征几乎是不可能的。只能比较它们在特定语言中的实现。我主要通过语言 Java 和 nodejs.
收集我的经验我观察到这些差异:
- 您可以使用
require
而不仅仅是让其他模块对您的模块可用。例如,您可以使用它来解析 JSON 文件。 - 您可以在代码中的任何地方使用
require
,而import
仅在文件顶部可用。 require
实际上执行所需的模块(如果尚未执行),而import
具有更多的声明性。这可能不适用于所有语言,但这是一种趋势。require
可以从子目录加载私有依赖项,而import
通常对所有代码使用一个全局命名空间。同样,这在一般情况下也不是真的,而只是一种趋势。
职责
如您所见,require
方法具有多重职责:声明模块依赖关系和读取数据。这最好与导入方法分开,因为 import
应该只处理模块依赖性。我想,您喜欢使用 require
方法来读取 JSON 的原因在于,它为程序员提供了一个非常简单的界面。我同意拥有这种简单的 JSON 阅读界面是件好事,但是没有必要将它与模块依赖机制混合在一起。可以有另一种方法,例如readJson()
。这将分离关注点,因此 require
方法仅在声明模块依赖项时才需要。
代码中的位置
现在,我们只使用 require
作为模块依赖项,在模块顶部以外的任何地方使用它是一种不好的做法。当您在代码中的任何地方使用它时,它只会让您很难看到模块依赖关系。这就是为什么您只能在代码之上使用 import
语句的原因。
我没有看到 import 创建全局变量的地方。它只是为每个依赖项创建一个一致的标识符,该标识符仅限于当前文件。正如我上面所说,我建议通过仅在文件顶部使用 require
方法来执行相同的操作。它确实有助于提高代码的可读性。
工作原理
加载模块时执行代码也可能是个问题,尤其是在大型程序中。您可能 运行 进入一个循环,其中一个模块可传递地需要自身。这真的很难解决。据我所知,nodejs 是这样处理这种情况的:当 A 需要 B 并且 B 需要 A 而你开始需要 A 时,那么:
- 模块系统记住它当前正在加载 A
- 执行A 中的代码
- 它记得当前正在加载 B
- 执行B 中的代码
- 它尝试加载 A,但 A 已经在加载
- A 尚未完成加载
- 它returns半载A到B
- B不希望A半载
这可能是个问题。现在,有人可以争辩说循环依赖确实应该避免,我同意这一点。但是,循环依赖只应避免在程序的独立组件之间。 类 在一个组件中经常有循环依赖。现在,模块系统可用于两个抽象层:类 和组件。这可能是个问题。
接下来,require
方法经常导致单例模块,不能在同一个程序中多次使用,因为它们存储全局状态。然而,这并不是系统的错,而是程序员以错误的方式使用系统的错。尽管如此,我的观察是 require
方法会误导新程序员这样做。
依赖管理
支持不同方法的依赖管理确实是一个有趣的点。例如 Java 在当前版本中仍然缺少合适的模块系统。再次宣布下一个版本,但谁知道这是否会成为现实。目前,您只能使用 OSGi 获取模块,这远非易用。
nodejs底层的依赖管理很强大。然而,它也不完美。例如,非私有依赖项,即通过模块 API 公开的依赖项,始终是一个问题。不过这个是依赖管理的通病所以不限于nodejs
结论
我想两者都没有那么糟糕,因为每个都被成功使用了。但是,在我看来,import
比 require
有一些 objective 优势,比如职责分离。由此可见 import
可以限制在代码的顶部,这意味着只有一个地方可以搜索模块依赖项。此外,import
可能更适合编译语言,因为这些语言不需要执行代码来加载代码。