Node开发掉坑记
最近做了些Node的开发,其中一个服务打包后,在生产环境中运行出错,所有的请求都报这个错误:
...
} has no method 'send'
at C:\a_very_deep_folder\helpers\express4\express4.js:1:1945
at Layer.handle [as handle_request] (C:\a_very_deep_folder\node_modules\express4\lib\router\layer.js:95:5)
at trim_prefix (C:\a_very_deep_folder\node_modules\express4\lib\router\index.js:312:13)
at C:\a_very_deep_folder\node_modules\express4\lib\router\index.js:280:7
...
找到出错的位置:
app.use(function (error, req, res, next) {
if (!error){
res.send(404);
}else{
console.error(error.stack);
res.send(500);
}
});
app.all('*', function (req, res, next){
//business
}
一看这是send无效啊,正好最近express升级到v4,可能跟这个有关,看看其他正常处理函数都是3个,这前面怎么还有个error,一定有问题,去掉!这样改,程序不出错了,能正常返回404。但是实际运行时express还会会有个警告:
express deprecated res.send(status): Use res.sendStatus(status) instead helpers\express4\express4.js:105:9
看来还是Express4有更新,改用sendStatus吧。
但是,不对啊,这光进404嘛意思,没进我注册的业务函数啊(应该通过app.all调用的)。想想Express4的逻辑,处理函数是按app.use、app.all的实现顺序来确定依次执行顺序的,如果一个处理函数不再调用next(),那一个对请求的处理就完成了。这里请求都被app.use响应了,如果把app.use放在app.all之后,那就正常了;试了下,果然可以。
我是不是做错了什么
但是细琢磨,这也不对啊;之前的代码都是跑过的,没遇到这么严重的问题啊;在本地运行一下也挺正常,根本没进原来4个参数的app.use;而修改过的3个参数的就每次都进,这什么情况,难不成express还带解析传入函数参数个数的?然后做了下测试,果然app.use有1~3个参数时,都会被执行,4及以上,就不会调用到了,会直接调用后面的app.all。
嗯,express有猫腻,来看看代码,router/index.js是它添加注册函数的地方:
//express4/lib/router/index.js
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
每个函数会实例化成为一个Layer,大概处理函数层次的意思。这里path就是注册响应的url路径,比如“/api/user”之类的,如果不传就是“/”,全部匹配了,因此前面的app.use就拦截了所有请求,都404了。再看看Layer的逻辑:
//express4/lib/router/layer.js
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
var fn = this.handle;
if (fn.length !== 4) {
// not a standard error handler
return next(error);
}
// call fn()
}
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
// call fn()
}
//express4/lib/router/route.js
if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
这里有两个处理函数,正巧是我怀疑的3、4个参数的判断,是通过function.length属性(这个不错啊,又学到了,从哪个版本开始支持的?)来判断的。实际上3参数是注册的正常处理函数;4参数是注册的错误处理函数。那这个明白了,原来代码的逻辑没什么问题,app.use完成的是最后错误兜底的工作。
可是,那还有什么区别呢?这又是在我机器上就没事儿的经典状况。正常Release打包,我们都会用混淆工具UglifyJS2来缩减一下代码体积,那跟这个有关系么?来看看处理后的app.use函数:
...
n.use(function(e,r,s){e?(console.error(e.stack),s.send(500)):s.send(404)})
...
这又是什么情况,原来的4个参数变成了3个参数;就是说把本来的错误处理函数被改成了正常处理函数,所以会出现最开始的错误。但是UglifyJS2为什么这么干,难道就因为第四个参数没用上么,这不地道啊?
寻找问题真正的原因
来,上UglifyJS2的github页面看看,看有人遇到这问题么、是不是改了,哪版改的。Issue里果然有发现:
Broken function.length if last argument is unused. #188,这遇到了跟咱同样的问题,通过function.length判断变量个数,但是无用的参数被截掉了。这个issue从2013年4月就开始讨论了:
- 正方(认可这个issue的用户)的观点是,做混淆不应该影响我业务逻辑啊。
- 反方(项目程序员?呵呵)的观点是,你这个function.length不是典型用法啊,我混淆还会改动函数名称呢,你难不成还用function.name做判断么?得举出更有说服力的例子才行。
- 正方给出了实际业务的用法、还有一个业务框架遇到的困境,判断函数参数在框架类代码里比较常见,终究所作的保护要比较多,function.length是比较方便的属性,而且也是JS语言标准之一。
期间还有反方逻辑不严格被正方抓出错来的,哈哈。然后这个问题在2014年以增加一个扩展参数来处理掉了,这个Commit:
Add option
keep_fargs
.By default it’s
false
. Passtrue
if you need to keep unused function arguments.Close #188.
master (#1) v2.4.24 … v2.4.13
mishoo authored on 8 Feb 2014
但是UglifyJS2的老大@mishoo(主程?)还是觉得这是个别情况,因此参数默认是不开启的。
I added a compressor option for it. To keep unused function arguments, pass keep_fargs: true to the compressor, for example:
uglifyjs file.js -m -c keep-fargs=true
I still believe that relying on this “feature” is silly, so it’s disabled by default.
再看一下咱用的UglifyJS2是2.4.14,那也加上这个参数用就好了。但是这事儿还没完,其实还有另外一个Issue讨论同样的问题,Removing unused arguments should be classed as unsafe? #121, 始于2014年2月。这里不依不饶的人更多,今年开始的讨论也更多了。直到正方@sc0ttyd给出了一个常用模块Lodash遇到的问题,而且是非function.length的例子。这终于开始能打动开发者了。
期间还扯到是不是做下代码的深度分析,显然谁都觉得这不太经济。最后,@mishoo在今年3月给出了这个Commit:
Keep unused function arguments by default
Discarding unused function arguments affects function.length, which can lead to some hard to debug issues. This optimization is now done only in “unsafemode”.
Fix #121
master (#1) v2.4.24 … v2.4.18
mishoo authored on 20 Mar
不裁剪函数变量成了默认逻辑,这样用起来就更方便了;如果用户清楚自己的代码,还是可以主动用unsafe的方式,继续裁剪无用变量。这个补丁从2.4.18开始,那我们就把UglifyJS2升级到2.4.24好了。
思考
然后咱回过来看看整个事件分析处理的过程,我一开始也在很粗糙的以为根据字面的错误提示,改改代码就过去了,但是引发了更严重的问题。就像这漫画的效果赛的:
图片来源: Sina App Engine官方微博
但是最终还是要去理解代码的含义,不能有侥幸心理。
那再来看看UglifyJS2上关于裁剪函数无用变量的讨论,过程很曲折,甚至显得有点儿扯淡是不是;但其实一点儿也不,这是一个特别正常的讨论过程,这就是软件开发。要让大家对一个功能点的逻辑有共识,确实是很困难的,特别是这边界还在不断发展中的。上面这俩Issue都是2013年初开始的,那时function.length大概还是比较新的特性、使用的人、场景、意义还不显得很大;但是到了2015年看来,可能就完全不一样了。
嗯,有空我也得翻翻自己项目的边界了。