CoffeeScript的全局变量污染与Node.js的模块加载
最近发现Continuation.js的一个Bug:命令行使用-c
开启缓存模式的时候,有时候更新了代码缓存不会更新,这个Bug时隐时现,难以捕捉。今天发现这个Bug升级了,不仅仅是在缓存模式的时候会有问题,即时没有开启-c
一样会发生这个问题。再到后来发现这个Bug只在CoffeeScript代码中出现,于是就锁定了目标开始调试。
Continuation.js和CoffeeScript一样支持动态加载编译,就是可以在Node.js中使用require
的方法加载原始代码,运行时编译。这样的好处不言而喻,给用户提供了一致而透明的接口,无需事先编译好再加载。具体的实现方法是,修改require.extensions
下面的回调函数,把加载定向到自定义的函数来处理,最后再调用require.main.compile
运行编译后的代码。
require
和module
是Node.js运行时的两个重要变量,所有模块的运行其实都是在一个这样的函数中的:
function(module, require) {
// Your code
}
所以require
和module
是模块内的全局变量。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(未发布)以后默认已经取消这个特性了。