林夕轩

立志于前端的超级菜鸟就是我

抓大放小的艺术——浏览器缩放检测

先对淘宝和QQ空间的检测行为进行比较:

淘宝:

  • 仅在用户缩放过再刷新页面之后提示一次,不会持续提示用户
  • 有人反馈在某个chrome版本下,淘宝的提示出现错误(原本没有缩放,但仍然提示缩放信息。同我们最初的方案)

QQ空间:

  • 不仅在新加载窗口后会计算缩放比例,且会持续更新状态
  • 在绝大多数浏览器中使用正常

再分享浏览器缩放检测的两个方案:

Flash

创建一个flash节点,请求一个swf文件。这个时候在创建节点时,可以设置该节点的宽、高、是否可拉伸等。当之后进行浏览器缩放时,比较flash的starge.height和节点本身的height。根据比例就能判断缩放程度了。

CSSOM

原理就是比较设备像素CSS像素。前者是一显示器的物理像素,后者是CSS的虚拟像素点。通过他们的比值就能判断页面是否被缩放。背景知识:两个viewport的故事
hack思路:webkit、IE、firefox均有方法计算当前设备像素。stack-overflow上有人进行整理了:zoom detct。同时,他提供了一个工具用来实现缩放的检测:detect-zoom。而这个工具正是前段时间,我们采用的工具。因为这个工具在2011年就已经停止更新了,所以对现代浏览器支持的能力是存疑的。

两种方案的比较

采用flash的优势是简单可用,它对于所有浏览器的表现是稳定的。检测的效果也比较明显。缺点是每次都得请求一个swf文件。 采用CSSOM的方案在于节省请求。但是他带来的问题有以下几点:

  • 需要针对不同浏览器进行hack,每个浏览器计算设备像素和CSS像素的API均不同。且同一浏览器的不同版本也有差异。
  • 有人说“当页面已经缩放然后再加载页面的时候,不能检测缩放”【存疑】

QQ空间方案

qq空间每次会加载一个accessory.js。这个JS会请求一段swf,创建一个用来计算缩放比例的flash节点。
逻辑如下:
埋点逻辑,设置初始宽高及不可拉伸:


a = QZONE.media.getFlashHtml({
    src: "http://" + imgcacheDomain + "/qzone/v6/accessory/plugin/zoom.swf",
    width: "10",
    height: "10",
    allowScriptAccess: "always",
    id: "accessory_zoom",
    name: "zoom_detect",
    wmode: "transparent",
    scale: "noScale"
});
QZFL.dom.createElementIn("div", document.body, !1, {
    id: "_qz_zoom_detect",
    style: "position: absolute; right: 0px; bottom: 0px; visibility: visible;"
}).innerHTML = a;

监听逻辑——显示提示信息


onZoomChange: function (a) {
    QZONE.FrontPage.noShareDb.get("close_detect_" + checkLogin(), function (b) {
        if (1 !=
            b) {
            b = this.currentScale = a.scale;
            var e = 1 < b ? "\u653e\u5927" : "\u7f29\u5c0f";
            if (1 != b)
                if ($j.ua.ie && 0.9 <= b && 1 > b) c.zoomDetect.isTipShowed && (QZONE.FrontPage.hideTopTips(), c.zoomDetect.isTipShowed = !1);
                else {
                    var d = QZFL.userAgent.macs ? "⌘+\u6570\u5b570" : "ctrl+\u6570\u5b570";
                    c.zoomDetect.level = b;
                    b = '<div class="gb_hintbar"><div class="inner"><i class="ui_ico ui_hint_warn"></i><div class="hintbar_txt">' + ("\u60a8\u7684\u6d4f\u89c8\u5668\u76ee\u524d\u5904\u4e8e" + e + "\u72b6\u6001\uff0c\u4f1a\u5bfc\u81f4\u7a7a\u95f4\u663e\u793a\u4e0d\u6b63\u5e38\uff0c\u60a8\u53ef\u4ee5\u952e\u76d8\u6309\u201c" +
                        d + "\u201d\u7ec4\u5408\u952e\u6062\u590d\u521d\u59cb\u72b6\u6001\u3002") + '<a href="javascript:;" onclick="QZONE.FP.zoomDetect.closeDetect();return false;">\u4e0d\u518d\u63d0\u793a</a></div></div><a title="\u70b9\u51fb\u9000\u51fa" class="text_close" href="javascript:;" onclick="QZONE.FrontPage.hideTopTips();return false;">\u00d7</a></div>';
                    1 > $j("#top_tips_container .gb_hintbar").length ? (QZONE.FrontPage.showTopTips(b, 60), TCISD && TCISD.pv("homeact.qzone.qq.com", "zoommode"), QZONE.qzEvent.dispatch("IC_FEEDS_POSITION_CHANGE")) :
                        $j("#top_tips_container").html(b);
                    c.zoomDetect.isTipShowed = !0
                } else c.zoomDetect.isTipShowed && (QZONE.FrontPage.hideTopTips(), c.zoomDetect.isTipShowed = !1)
        }
    })
},

调用逻辑——定义回调

var a = c.zoomDetect;
QZONE.FP.zoomDetect = c.zoomDetect;
window.zoomDetectCallback = c.zoomDetect.onZoomChange;

淘宝方案

核心逻辑——CSSOM

detectZoom: function () {
   function t(t, e) {
      var r = Math.pow(10, e);
      return Math.round(t * r) / r
   }
   var e = 1,
   r = v.devicePixelRatio;
   return r && 1 > r && r > 0 ? (e = r, f.dpr = r) : isNaN(screen.deviceXDPI) || isNaN(screen.logicalXDPI) ? v.outerWidth > 0 && v.innerWidth > 0 && (f.outerWidth = v.outerWidth, f.innerWidth = v.innerWidth, e = v.outerWidth / v.innerWidth) : e = screen.deviceXDPI / screen.logicalXDPI, f.zoom = e, 1 !== t(e, 1)
}

虽然进行了变量替换,但是还是不难发现淘宝正是使用了CSSOM的方法。主要的判断逻辑是用了两组比值来计算页面缩放。

  • screen.deviceXDPI / screen.logicalXDPI:这是IE8使用的设备像素与CSS像素的比值
  • window.outerWidth / window.innerWidth:这是webkit、gecko使用设备像素与CSS像素的比值

正常情况

在最新版的chrome和safari是能够识别的,因为webkit内核的计算方法比较统一。但这不保证老版本的chrome是否出现计算错误的情况。

异常情况

  • 火狐的outerWidth和innerWidth永远相等。无法判断
  • IE8之前的浏览器不能判断(不能缩放)

结论

通过对两种方案的研究,发现使用Flash的效果更好,虽然会多发一个请求。但如果放在加载之后请求,对页面的性能影响也不大。而是用CSSOM的方案,目前没有一个靠谱的工具,如果我们自己开发,就得考虑所有浏览器所有版本的技术方法。

具体实施

采用CSSOM,仅支持比较靠谱的浏览器——IE8以上及Chrome21以上,使用这些浏览器的用户已经覆盖了大概85%美团用户。
使用之前IE和chrome的两套API进行判断:

  • 如果进入页面时就已经缩放了就立即提示,并监听组合键用来识别用户是否使用Ctrl + 0,并且持续监听后续用户缩放页面的事件。
  • 反之,当用户访问美团的时候没有缩放页面,这时候不需要进行缩放检测。因为只有用户在访问过程中使用了组合键才会让页面缩放,而这时候用户是明确知道自己缩放了页面的

需要注意的点

  • 如何抓大放小?
    对于不在检测对象之内的浏览器,通通放掉。反之,符合的浏览器,也只有在正常的缩放比例内(大于1.1倍,小于0.9倍)才显示,因为不同浏览器求出的CSSOM比值是和理论值是不完全一致的。
  • Chrome预加载如何处理?
    Chrome的预加载功能会提前渲染整个页面,但奇怪的是目前的版本中Chrome计算的window.outterWidth是0。我们最好的方法就是对于这种特殊情况都不进行检测提醒。
  • 缩放检测的重点是大而全还是小而精?
    使用CSSOM的方法,就注定做不到大而全,所就要把小而精做到极致——绝对不能错提示。我们对于存疑的检测结果,不会提示用户。而对于确定的结果,显示的结果一定要正确。这么做的理由是:缩放检测是一个增强型功能,我们必须在不影响用户的基础功能前提下,用以增强体验。

还想说的话

之前第一版的浏览器缩放使用了一个外部js,当时的效果惨不忍睹,每隔一段时间都会有用户反馈缩放提醒错了。当时觉得要满足所有用户的需求,实在太难了,操作系统和浏览器的笛卡尔集实在太过庞大。
后来,想到这种抓大放小的策略确实是一门艺术。如何让这种非必需功能来服务高级用户是一个不错的思路。
非常感谢我的领导们,支持我做着一系列调用,并给出一些意见。有一些如此开明的领导支持我们做一些额外的尝试是一件很快乐的事情。
欢迎大家投递简历到zhangmengxuan@meituan.com,我帮你内推美团前端职位!

2014-10-28

浏览器 缩放检测