Zepto 源码分析 2 - Polyfill 设计

在进入 Zepto Core 模块代码之前,本节简略列举 Zepto 及其他开源库中一些 Polyfill 的设计思路与实现技巧。

涉及模块:IE/IOS 3/Detect.

IE 模块 / CSSOM 相关 Polyfill

Zepto 的 IE 模块 src/ie.js 中仅仅包含了一个兼容性降级逻辑,虽简单其实现也值得学习:

(function() {
  try {
    getComputedStyle(undefined);
  } catch (e) {
    var nativeGetComputedStyle = getComputedStyle;
    window.getComputedStyle = function(element, pseudoElement) {
      try {
        return nativeGetComputedStyle(element, pseudoElement);
      } catch (e) {
        return null;
      }
    };
  }
})();

低版本兼容模式(以 IE 7 为例)调用 getComputedStyle 会出现找不到该方法的问题),已经在高版本 IE 获得支持

The value of the property 'getComputedStyle' is null or undefined, not a Function object

顾名思义该方法用于获得元素的动态计算属性,此处在 windows 对象上显式挂载包含 Failover 的 getComputedStyle 方法,使得该方法不存在时的调用代码仍可继续运行,不行成阻塞。更详细的多浏览器兼容方案可以通过阅读 jQuery css API 源码找到。

此模块包含的设计思路即为Failover 预 catch以匹配降级方案

从该问题中可以引申出一个常见问题,CSSOM 的浏览器支持程度远远低于 DOM 的支持程度,W3C 对于 Document Object Model (DOM) Level 2 Style Specification 的声明早已于 2000 年末时刻完成,然而 CSSOM 的官方标准 CSS Object Model (CSSOM) 由于 CSS 3 多管道演进的实现方式影响,仍未推出厂商公认的实际标准,因此对于 CSSOM 的操作设计与跨浏览器兼容性测试,jQuery 仍有极好的参考价值。同时,开源社区中也存在大量的 Polyfill(腻子脚本)用于对低版本浏览器通过 JavaScript 附加逻辑的方式附加较新潮的特性,可以在 Modernizr/Modernizr 类似的代码源中找到。阅读 Polyfill 往往可以获得对 原型链和 JS 面向对象设计思维的更深刻认识,以及更深层次的设计技巧,以如下的一个 IE 8 opacity 属性的 Polyfill 函数为例,完成该函数的技巧已经远远超越了自身实现的功能:

//\ 正则表达式,匹配满足 alpha 定义规则的字符串
var opacityre = /\b\s*alpha\s*\(\s*opacity\s*=\s*(\d+)\s*\)/;

//\ 原型链挂载,直接将 opacity 放入 CSSStyleDeclaration 中
defineProperty(window.CSSStyleDeclaration.prototype, "opacity", {

  //\ getter 函数,自定义 toString 方法
  get: function() {
    var m = this.filter.match(opacityre);
    return m ? (m[1] / 100).toString() : "";
  },
  
  //\ setter 函数,将 opacity 值写入 alpha(opacity=$value) 的形式,供浏览器使用
  set: function(value) {
    this.zoom = 1;
    var found = false;
    if (value < 1) {
      value = " alpha(opacity=" + Math.round(value * 100) + ")";
    } else {
      value = "";
    }
    this.filter = this.filter.replace(opacityre, function() {
      found = true;
      return value;
    });
    if (!found && value) {
      this.filter += value;
    }
  }
});

此脚本包含的设计思路为利用 Getter/Setter 控制不同上下文中属性的设置与获取,同样的思路即为 Vue.js 数据绑定的设计源泉。

IOS 3 模块 / 语言特性 Polyfill

Zepto 默认编译中未包含的 IOS 3 模块 src/ios3.js 包含了两个函数的兼容实现,实际上属于语言特性 Polyfill,这类 Polyfill 主要用于解决语言发展与实现不同步等问题,并提供一些实现良好的公共方法用于业务开发,最常见的两类例子:

  • Lodash / Underscore 提供大量实现良好的工具函数
  • TypeScript 提供类型系统,实际这门语言也可被当做 Polyfill 看,因为 ECMAScript 提案中,已经包含了一个静态类型系统 的建议

IOS 3 模块中的两个 Polyfill 分别为 String / Array 两个包装类原型上挂载了一个常见方法:

//\ Line 6
    String.prototype.trim = function() {
      //\ 将字符串首末的空格剪除
      return this.replace(/^\s+|\s+$/g, "");
    };

该方法原始定义于 ES 5 标准中的 String.prototype.trim(),此处实现依赖 ES 5 标准中的 White Space 中的描述。该方法实现相对简单,同时也提示了一个设计常识:向公认的 API 靠齐,实现方法核心后提供方法扩展,遵循该原则的包括:

  • Preact 与 React
  • Zepto 与 jQuery
  • Lodash 与 Underscore 等

trim() 函数较为简单明确,而 reduce() 方法的实现与 ES 5 中的 Array.prototype.reduce(callbackfn[,initialValue]) 定义的算法完全相同,更能体现这一原则,此段不进行注释,进入 ES 5 规范中该函数定义即可对照理解该 Polyfill 的实现方法。

//\ Line 11
  if (Array.prototype.reduce === undefined)
    Array.prototype.reduce = function(fun) {
      //\ 略
    };

Detect 模块 / User Agent 识别

Detect 模块用于识别浏览器平台类型,默认也不处于编译列表中,其代码 src/detect.js 组织结构如下:

//\ Line 5
;(function($){

  //\ 平台侦测逻辑
  function detect(ua, platform){
  }

  //\ 传入 Zepto 及平台环境变量
  detect.call($, navigator.userAgent, navigator.platform)
  // make available to unit tests
  $.__detect = detect

//\ 将全局变量 Zepto 带入,化为参数 '$'
})(Zepto)

平台侦测逻辑 function detect(ua, platform) 内部为一组大的字符串判断逻辑,形成这样杂乱无章的平台判断逻辑,正是因为一代一代的浏览器大战。 User-Agent 字符串被定义为包含了当前浏览器(规范名称 User Agent)信息的 HTTP 头部标识,用于使服务器可以根据平台完成浏览器检测并下发不同的原始代码用于渲染。由于浏览器伪装等各种原因,UA 实际并不可信,因此对于它的侦测相当困难,常见 UA 可以从 List of User Agents 页面内查询到。

Zepto 没有默认编译该模块,以及利用该模块判断后提供平台相关逻辑的主要原因在于其设计原则:20% 的代码完成 jQuery 核心 80% 的功能。此处,也引出了代码实现的另一个基本原则:面向功能/API 标准,先功能覆盖再优雅降级。以提供一个常见的 Browser Compatibility Matrix 为例,根据实现规格测试前端产出在不同平台的可用性,再提供降级方案或 Polyfill 以满足更多的用户需求。

相关推荐