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)

总结下来,导致兼容性代码的因素有:

  1. 特性支持不全,比如 IE8- 不支持 getElementsByClassName. 这是最好解决的,给 IE 实现一个就行。
  2. 特性都支持,但有命名/调用接口或其它细节上的差异。典型是 DOM Range 接口。如果大家都支持 IE 的标准,世界很美妙。如果大家都支持 Mozzila 的接口,世界也很美妙。但现实世界是,大家各自实现自家的标准,细节差异一堆,杯具啊。所以做编辑器的,比如 FCKEditor, 干脆自己用 dom 操作再实现一套代码,完全不依赖各个浏览器的实现。要统一,得消灭。秦始皇焚书坑儒,估计也是被逼的。
  3. 还一种兼容代码,是设备差异引起的。比如桌面浏览器和 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, 一切让人往好的方向思考的尝试,都是值得钦佩的。

总之,一切皆权衡。