Beyond the Void
BYVoid
CoffeeScript的全局变量污染与Node.js的模块加载
本文简化字版由OpenCC转换

最近发现Continuation.js的一个Bug:命令行使用-c开启缓存模式的时候,有时候更新了代码缓存不会更新,这个Bug时隐时现,难以捕捉。今天发现这个Bug升级了,不仅仅是在缓存模式的时候会有问题,即时没有开启-c一样会发生这个问题。再到后来发现这个Bug只在CoffeeScript代码中出现,于是就锁定了目标开始调试。

Continuation.js和CoffeeScript一样支持动态加载编译,就是可以在Node.js中使用require的方法加载原始代码,运行时编译。这样的好处不言而喻,给用户提供了一致而透明的接口,无需事先编译好再加载。具体的实现方法是,修改require.extensions下面的回调函数,把加载定向到自定义的函数来处理,最后再调用require.main.compile运行编译后的代码。

requiremodule是Node.js运行时的两个重要变量,所有模块的运行其实都是在一个这样的函数中的:

function(module, require) {
  // Your code
}

所以requiremodule是模块内的全局变量。require.extensions是一个对象,用于根据扩展名注册require的回调函数,默认情况下,require.extensions是这样的(Node.js 0.10.12):

{
  '.js': function (module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
    module._compile(stripBOM(content), filename);
  },
  '.json': function (module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
    try {
      module.exports = JSON.parse(stripBOM(content));
    } catch (err) {
      err.message = filename + ': ' + err.message;
      throw err;
    }
  },
  '.node': function () { [native code] }
}

在Node.js代码中使用require(filename)时,实际上会根据filename的后缀扩展名依次来选择加载的回调函数,例如我想增加一种自定义的自动加载类型.byv,只需设置require.extensions['.byv']即可。同理,也可以修改已有的后缀的加载函数,Continuation.js就是这么做的(修改了.js文件的加载函数)。

为了透明支持CoffeeScript,Continuation.js修改了.coffee的加载函数,在自定义的回调函数中调用CoffeeScript模块,调用CoffeeScript编译,然后再使用Continuation.js编译。问题就在这里,是加载CoffeeScript的时候require.extensions['.coffee']被修改了。阅读CoffeeScript的代码(版本1.6.3),发现在'coffee-script'模块中,有这么几行代码:

if require.extensions
  for ext in ['.coffee', '.litcoffee', '.coffee.md']
    require.extensions[ext] = loadFile

这段代码不应该在加载CoffeeScript模块中运行,而应该在通过命令行运行coffee命令的时候运行,可惜CoffeeScript没有注意到这一点,应该算是一个Bug吧。给CoffeeScript提交了一个推送请求https://github.com/jashkenas/coffee-script/pull/3054 ,等待审核中。

更新:这个issue被标注为重复,已经在 https://github.com/jashkenas/coffee-script/issues/2323 合并了,CoffeeScript 2.0.0(未发布)以后默认已经取消这个特性了。


上次修改时间 2017-03-16

相关日志