infinte 总是能给我们带来一些新思路新想法:更优雅的兼容
很不错的思路。不过实际操作时,并不好组织。比如:getOffset (获取 elem 相对 page 的偏移量)方法,对于高级浏览器,直接 getBoundingClientRect + win.scrollLeft/Top 即可。对于低级浏览器,比如 Safari 2, 得利用 offsetParent 不断向上回溯叠加。至此,利用文中提及的优雅兼容,可构造:
nullDriver = {}; dhtmlDriver = derive(nullDriver); w3cDriver = derive(dhtmlDriver); if(supportsGetBoundingClientRect) { w3cDriver.getOffset = function() { ... } } else { dhtmlDriver.getOffset = function() { ... } }
看起来很美妙,可是问题不这么简单。w3cDriver.getOffset 里,依旧还有浏览器差异,比如在同是 webkit, 桌面版和 ipad 版是有差异的,并且郁闷的是,这个差异不大,就那么一两行代码。传统写法:
w3cDriver.getOffset = function() { ... if(isAppleMobileWebkit) { // bug fix } else { // go on } ... }
按照优雅思路,上面的代码很 ugly, 一个可能的重构:
appleMobileWebkitDriver = derive(w3cDriver) appleMobileWebkitDriver.getOffset = function() { ... }
但这样的话,appleMobileWebkitDriver.getOffset 和 w3cDriver.getOffset 两个方法中将存在大量冗余代码。如何避免,获取可以利用 super.method:
appleMobileWebkitDriver.getOffset = function() { w3cDriver.getOffset(); // fix code }
但问题并不如此简单,fix code 有可能并不能简单放在 super.method 前面和后面。很可能得拆分:
w3cDriver.getOffsetPartA = function() { ... } w3cDriver.getOffsetPartB = function() { ... } appleMobileWebkitDriver.getOffset = function() { w3cDriver.getOffsetPartA(); // fix code w3cDriver.getOffsetPartB(); }
(注意:对于 getOffset 来说,是可以仅重复少量代码,来使得 fix code 放在后面即可,上面仅是示例)
光拆分还不行,还得合并:
w3cDriver.getOffset = function() { this.getOffsetPartA(); this.getOffsetPartB(); }
可以看出,原本两个 if else 可以搞定的事情,用优雅兼容思路重构后,代码量增加了很多,一定程度上,更晦涩了。
稍等,上面的代码还未考虑 dhtmlDriver.getOffset 的具体实现。现实世界里,这里面的差异更丰富多彩:
// from Ext 3.2 getXY : function(el) { ... if (Ext.isGecko) { ... } ... if (Ext.isSafari && hasAbsolute) { ... } ... if (Ext.isGecko && !hasAbsolute) { ... } ... if (!Ext.isOpera || (...)) { ... } ... }
这真是一个糟糕的世界。采用 driver 思路,或许得构造出:
safariHasAbsDriver = derive(dhtmlDriver); geckoNoAbsDriver = derive(dhtmlDriver); nonOperaDrive = derive(dhtmlDrive);
还得拆分 dhtmlDriver.getOffset 方法,想想都麻烦,就不必再继续演示了。或许可以放在 dhtmlDriverBugfix 里,但这样的话,仅是将代码转移了一个地方,里面依旧达不到优雅。
等等,一开头我们还犯了个错误,getBoundingClientRect 其实是 IE 的标准,其它浏览器看着好,就抄袭过去(IE 当年的霸气,至今依旧在)。W3C 看大家差不多都实现了,于是写入标准。基于这个考虑,或许应该这么命名:
defactoDriver = derive(nullDriver) fallbackDriver = derive(defactoDriver)
总结下来,导致兼容性代码的因素有:
- 特性支持不全,比如 IE8- 不支持 getElementsByClassName. 这是最好解决的,给 IE 实现一个就行。
- 特性都支持,但有命名/调用接口或其它细节上的差异。典型是 DOM Range 接口。如果大家都支持 IE 的标准,世界很美妙。如果大家都支持 Mozzila 的接口,世界也很美妙。但现实世界是,大家各自实现自家的标准,细节差异一堆,杯具啊。所以做编辑器的,比如 FCKEditor, 干脆自己用 dom 操作再实现一套代码,完全不依赖各个浏览器的实现。要统一,得消灭。秦始皇焚书坑儒,估计也是被逼的。
- 还一种兼容代码,是设备差异引起的。比如桌面浏览器和 mobile 浏览器,不少地方,理念上都发生变化,代码实现上,肯定得变。
再总结下目前常用的兼容代码的写法技巧:
代码组织上,用 if else / switch 还是其它。在大量重构书籍里,会告诉你,switch 和 if else 往往代表着某种坏味道,经常可以用工厂模式或对象模式(我杜撰的,对应 js 里的 { condition: code } 的分支重构法)等方式重构掉。但我觉得,如果 if else 并没引起混乱,看起来依旧清晰时,是不用重构的。计算机语言里,创造了 if else, 就不要排斥。
分支判断上,浏览器嗅探和特性探测。浏览器嗅探,这个大家都很熟悉,但最近一两年来,以 John Resig 为代表的一批先锋,开始抨击浏览器嗅探的种种不是。jQuery 里,直接不推荐再调用 browser, 全面改用 support. 但我觉得,事情没有这么绝对。浏览器嗅探就和 table 一样,table 布局遭受抨击,但并不意味着,就不能用 table. 该用 table 的地方,还是得大胆用。浏览器嗅探也如此。具体原因,看下一条。
进一步分析第二点,特性探测的好处究竟是什么?特性探测的好处是能自动适应未来设备和未知设备,比如 if(document.addEventListener) 假设 IE9 支持标准事件,则代码不用修改,就自适应了“未来浏览器”。对于未知浏览器也是如此。但是,这并不意味着浏览器嗅探就得彻底抛弃。当代码很明确就是针对已知特定浏览器的,或者并非是某个特性探测可以解决时,用浏览器嗅探反而能带来代码的简洁,同时也也不会有什么后患。
忘了哪位达人曾说过:每一个优雅的架构下面,都有一堆龌龊的实现。最近看陈冠中的《盛世 - 2013》,里面提到莱布尼茨的一句话挺有意思:在所有可能的世界中的最好的一个世界里,一切都已经是最好的了。因此优雅的架构和想法下面,龌龊的实现,或许已经是最好的了。当然,我们不能放弃对更好的追求。最后感谢 infinte, 一切让人往好的方向思考的尝试,都是值得钦佩的。
总之,一切皆权衡。