【问题标题】:coffee require() statements causing circular references?导致循环引用的咖啡 require() 语句?
【发布时间】:2013-01-11 00:22:58
【问题描述】:

我遇到了一个奇怪的问题,Coffeescript 没有正确缓存/完成加载循环引用的 require() 语句。

当我使用 node main.js 运行此代码时...

ma​​in.js

module.exports = {
    name: 'John'
}

var config = require('./config');
config.hello();

config.js

var main = require('./main');

module.exports = {
    hello: function() {
        console.log("Hello " + main.name);
    }
}

...我得到以下输出:

你好约翰

但是,当我使用 coffee main.coffee 运行等效的咖啡脚本代码时...

ma​​in.coffee

module.exports =
    name: 'John'

config = require './config'
config.hello()

config.coffee

main = require './main'

module.exports =
    hello: ->
        console.log "hello #{main.name}"

...我明白了:

TypeError: Object # has no method 'hello'

当我将代码编译为纯 ol' Javascript 并通过节点运行时,这很好。

咖啡怎么了?

【问题讨论】:

  • afaik 咖啡脚本必须在执行前编译。
  • 确实如此,但是 npm install -g coffee-script 免费提供的“咖啡”应用程序是产生上述行为的原因——我每次都想避免编译周期测试开发代码更改

标签: node.js coffeescript


【解决方案1】:

我的理论是这种行为是 node.js require 缓存如何工作、node 如何处理循环依赖项以及咖啡转译器在将.coffee 文件直接加载到 node 时如何工作的组合。

我制作了不同的coffeescript 和javascript 版本的程序,并带有如下说明性日志记录。我使用“maincs”和“mainjs”来确保没有混合两种语言。

ma​​injs.js

console.log("mainjs starting");
console.log("mainjs exporting name");
module.exports = {
  name: 'John'
};
console.log("mainjs requiring configjs");

var configjs = require('./configjs');

console.log("mainjs calling configjs.hello()");

configjs.hello();

configjs.js

console.log("configjs starting");
console.log("configjs requiring mainjs");
var mainjs = require("./mainjs");
console.log("configjs exporting hello");
module.exports = {
  hello: function() {
    return console.log("hello " + mainjs.name);
  }
};

ma​​incs.coffee

console.log "maincs starting"
console.log "maincs exporting name"
module.exports =
  name: 'John'

console.log "maincs requiring configcs"
configcs = require './configcs'
console.log "maincs calling configcs.hello()"
configcs.hello()

configcs.coffee

console.log "configcs starting"
console.log "configcs requiring maincs"
maincs = require "./maincs"
console.log "configcs exporting hello"
module.exports =
  hello: ->
    console.log "hello #{maincs.name}"

所以当我们运行它们时,我们会得到不同的输出(如您所见)。我在下面突出显示了有趣的部分。

node mainjs.js
mainjs starting
mainjs exporting name
mainjs requiring configjs
configjs starting
configjs requiring mainjs   #<--- Note the top-level mainjs.js code does not re-execute
configjs exporting hello
mainjs calling configjs.hello()
hello John

coffee maincs.coffee 
maincs starting
maincs exporting name
maincs requiring configcs
configcs starting
configcs requiring maincs
maincs starting      # <-- Look, the top-level maincs.coffee code is re-executing
maincs exporting name
maincs requiring configcs
maincs calling configcs.hello()
TypeError: Object #<Object> has no method 'hello'

所以我认为这种行为与 node.js 要求系统模块缓存的工作方式和 transpile-on-the-fly coffeescript 解释器有关。基本上,如果maincs = require "maincs" 导致重新执行maincs 模块中的顶级代码,我们会遇到循环依赖情况,节点将向maincs 提供configcs 导出对象的未完成副本.

请阅读node.js documentation on cyclic dependencies that explains this behavior(至少部分)。

现在,有趣的是,如果您确保 hello 函数被导出您需要 maincs 之前,您可以部分解决这个问题。但是,在 hello 代码中引用 main.name 将不起作用,因为 main 在第一次执行时将是未定义的。

ma​​incs2.coffee

console.log "maincs2 starting"
console.log "maincs2 exporting name"
module.exports =
  name: 'John'

console.log "maincs2 requiring configcs2"
configcs2 = require './configcs2'
console.log "maincs2 calling configcs2.hello()"
configcs2.hello()

configcs2.coffee

console.log "configcs2 starting"
console.log "configcs2 exporting hello"
module.exports =
  hello: ->
    console.log "hello from configcs"
console.log "configcs2 requiring maincs2"
maincs2 = require "./maincs2"

coffee maincs2.coffee
maincs2 starting
maincs2 exporting name
maincs2 requiring configcs2
configcs2 starting
configcs2 exporting hello
configcs2 requiring maincs2
maincs2 starting
maincs2 exporting name
maincs2 requiring configcs2
maincs2 calling configcs2.hello()
hello from configcs
maincs2 calling configcs2.hello()
hello from configcs

所以基本上,这种行为是require 缓存模块的方式、与循环依赖项的交互方式以及咖啡转译器本身的某些方面的结合。请注意,将 .coffee 转换为 .js 并使用 node 执行可以避免在具有 IIFE 包装器的常规咖啡脚本和没有包装器的裸咖啡脚本中出现此问题,因此 IIFE 包装器似乎无关紧要(无论如何,节点基本上都会添加自己的)。

【讨论】:

  • 很棒的细节,谢谢!除了重新设计模式或恢复为普通 JS 之外,还有什么“治愈方法”?
  • 通常循环依赖是一种设计味道。如果您有一个名为“config”的模块,它可能不需要主模块。我认为您可以通过检测它何时发生并重新要求模块来在coffeescript中明确编码。不过还没试过。
  • 我正在使用“express”,并且需要访问全局环境变量 - 理想情况下,无需将它们显式设置为全局。这是我看到很多基于 express 的应用程序使用的设计模式,尽管可能有更好的方法来做到这一点。
  • 恕我直言,这是coffeescript中的一个错误——如果不在转译器中,那么肯定在声称1-1兼容性的文档中。被举报了吗?
猜你喜欢
  • 2014-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-18
  • 1970-01-01
  • 1970-01-01
  • 2012-06-14
相关资源
最近更新 更多