基于设备检测的移动设备网页优化
背景
前段时间遇到一个Bug,一群人纠结了很久,最后终于找到了问题的所在,在此记录下来算是总结吧!
Bug描述:在同一个git branch的两个不同手机产品Phone1和Phone2,他们的Browser模块拥有相同的代码。但是这两个产品拥有不同UA Header。
在使用Browser访问网址:http://m.o2.de/goto/tbm-bild 时,Phone1能够正常显示网页,但是Phone2却显示出错信息(Please enable JavaScript in your browsers settings.)。
UA Header:全名是Useragent Header。拿手机举例,UA Header用来标识手机的型号(例如:Nokia 204)。在使用手机Browser访问网页或者进行其他Http链接的时候,手机端会把手机的型号按照Http规范拼接成类似‘Nokia 515/2.0(p)’的字串并添加到Http request里面发送到WebServer端。WebServer端则根据不同的手机型号对返回的网页数据进行定制。
问题分析
这个问题的奇怪之处就在于同属于一个git branch的phone1能够正常访问网页,但是Phone2却显示出错信息。那Phone1和Phone2有些什么区别呢?原来Phone1和Phone2虽然拥有相同的上层代码,但由于产品硬件的不同,却拥有不同的Driver层代码和产品配置信息。由于Browser使用Http协议获取和解析网页数据的代码都属于上层代码与Driver层没有太直接的关系。所以,大概可以排除由于产品Driver层代码差异的这种可能。把分析的思路放在产品配置信息上。
为了确定是那些产品配置信息导致了这个问题,我们使用了网络抓包工具Wireshark获取了Phone1和Phone2在使用Browser访问该网站时的Protocol信息。
(1)Phone1
(2)Phone2
通过对比发现Phone1和Phone2在和WebServer交互的前两对Request和Response都是一样的。
第1对Request和Response将网址:http://m.o2.de/goto/tbm-bild 重定向到:http://m.o2online.de/goto/tbm-bild
第2对Request和Response将网址:http://m.o2online.de/goto/tbm-bild 重定向到:http://wap.bild.de/?nbsrc=o2_w2g_textlink
第3对Request和Response中,WebServer给Phone1和Phone2返回了不同的数据如下:
Phone1:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.2//EN" "http://www.wapforum.com/dtd/wml12.xml"> <wml> <head> <meta name="keywords" content="Bild-Channel Home"/> <meta name="description" content="Aktuelle Nachrichten aus den Bereichen News, Leute, Sport, Auto und mehr"/> </head> <card title=" Home - BILD.de MOBIL"> <!--wd_ok--> <p> <br/><br/><br/> <a href="/"> <img src="http://bilder.bild.de/fotos-skaliert/logo-2013--30727524/14,w=64,c=0,p=0,f=0,format=jpg,fsize=12000.bild.png" width="64" alt="Bild.de"/> </a> <br/> <a href="/bild/json.bild.de/servlet/json/wap/18496602/4.html?i=0&p=2062659024">07:03 Uhr Super Bowl: Broncos vs. Seahawks</a> <br/> <a href="/bild/json.bild.de/servlet/json/wap/34307694/17.html"> <img src="http://bilder.bild.de/fotos-skaliert/a-dschungelcamp-mitternacht_36439853-1390172259-34311730/2,w=101,c=0,p=0,f=0,format=jpg,fsize=12000.bild.jpg" width="101" alt=""/></a> <br/><br/> <a href="/bild/json.bild.de/servlet/json/wap/34308038/12.html"> ...... <!--wd_ok--> </p> <do label="zurück" type="prev"> <prev/> </do> </card> </wml>
Phone2:
<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script type="text/javascript"> var sURL = "http://wap.bild.de/"; var sQuery = "nbsrc=o2_w2g_textlink"; var method = "inner width"; var cookieDomain = ""; var checkForCookie = true; var checkForSize = false; var originalReferer = ""; var canReload = true; var canDisabledCookies = false; var cacheKey = ""; var detectImmediately = false; var cookieLifetime = null; var connectiontype = function() { var ct, t; ct = "unknown"; try { for (t in navigator.connection) { if (t != "type" && navigator.connection.type === navigator.connection[t]) { ct = t; } } } catch (ex) { } return ct; } var getQueryVariable = function(variable) { var query, vars, i, pair, val; try { query = window.location.search.substring(1); vars = query.split("&"); for (i = 0; i < vars.length; i++) { pair = vars[i].split("="); if (pair[0] === variable) { val = pair[1]; i = vars.length; } } } catch (ex) { } return val; } var cookiesupport = function() { var support, cvalue, readUrl; support = false; cvalue = 0; try { if (document.cookie) { var cookies = document.cookie.split("; "); var output = ""; for ( var i = 0; i < cookies.length; i++) { var cookiesplit = cookies[i].split("="); if (cookiesplit[0] === "emvcc") { cvalue = cookiesplit[1]; } } } if (cvalue === "true" || cvalue === "1") { support = true; } readUrl = getQueryVariable("emvcc"); if (readUrl === "false" || readUrl === "0") { support = false; } } catch (ex) { } return support; } var pixelratio = function() { var pr = 1; try { pr = window.devicePixelRatio; if (pr) { pr = parseFloat(pr, 10); } else { pr = 1; } } catch (ex) { } return pr; } var viewportsize = function(method) { var width, heigth; width = window.innerWidth; height = window.innerHeight; try { if (method === "inner width") { width = window.innerWidth; height = window.innerHeight; } else if (method === "outer width") { width = window.outerWidth; height = window.outerHeight; } else if (method === "avail width") { width = screen.availWidth; height = screen.availHeight; } else if (method === "body client width") { width = document.body.clientWidth; height = document.body.clientHeight; } else if (method === "document client width") { width = document.documentElement.clientWidth; height = document.documentElement.clientHeight; } } catch (ex) { } return width + "x" + height; } function doLoad() { setTimeout("executeTest()", 100); } function cookify(name, value) { var c, d, l, e; e = ""; if (cookieLifetime) { l = 1000 * cookieLifetime; d = new Date(); d = new Date(d.getTime() + l); e = ' expires=' + d.toGMTString() + ';'; } c = name + '=' + value + '; path=/;' + e; if (cookieDomain && cookieDomain.length > 0) { c = c + " domain=" + cookieDomain; } return c; } function executeTest() { var pr = pixelratio(); var ct = connectiontype(); var nbcol = '1|' + ct; nbcol = window.escape(nbcol); var vpsize = viewportsize(method); var useCookie = cookiesupport(); var ad = ""; var anchor = ""; if (document.location && document.location.hash) { anchor = document.location.hash; } if (useCookie === true) { document.cookie = cookify('nborh', originalReferer); document.cookie = cookify('nbpr', pr); document.cookie = cookify('nbcol', nbcol); if (checkForSize) { document.cookie = cookify('emvAD', vpsize); } if (checkForCookie) { document.cookie = cookify('emvcc', 1); } var index = sURL.indexOf(";jsessionid="); if (index > -1) { sURL = sURL.substring(0,index); } sURL = sURL + (sQuery.length > 0 ? "?" + sQuery:""); var cookiemess = canDisabledCookies && checkForCookie; if (canReload && !cookiemess) { window.location.reload(true); } else { if (cookiemess) { sURL = sURL + (sQuery.length > 0 ? "&emvcc=0" : "?emvcc=0"); } window.location.replace(sURL); } } else { var connector = "?"; if (sURL.indexOf("?") >= 0) { connector = "&"; } ad = connector + "nborh=" + originalReferer; if (checkForSize) { ad = ad + "&emvAD=" + vpsize; } if (checkForCookie) { ad = ad + "&emvcc=0"; } ad = ad + "&nbpr=" + pr; ad = ad + "&nbcol=" + nbcol; if (cacheKey !== "") { ad = ad + "&nbck=" + cacheKey; } sURL = sURL + ad + (sQuery.length > 0 ? "&" + sQuery : "") + anchor; window.location.replace(sURL); } } if (detectImmediately) { executeTest(); } </script> <noscript> <meta http-equiv="refresh" content="1; url=http://wap.bild.de/?nbsrc=o2_w2g_textlink&emvcc=0&emvAD=240x320&nbcol=0%7Cunknown" /> </noscript> <style> body { color:#ffffff; background-color:#ffffff; padding: 0px; margin-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; font-family: Arial, Helvetica, sans-serif; font-size:11px; } div { color:#ffffff; background-color:#ffffff; margin-left: 5px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; } .text { color:#ffffff; background-color:#ffffff; font-size:11px; } </style> </head> <body onload="doLoad();"> <!-- Copyright 2013, Netbiscuits GmbH. External usage of this data without granted permission is strictly prohibited. --> <div class="text"> <br/><br/><br/><br/><br/><br/> <br/><br/><br/><br/><br/><br/> <span style="color:#ffffff; background-color:#ffffff;">--------------------------------------------------------------------------------------------------------</span><br/><br/> <span style="color:#ffffff; background-color:#ffffff;">Detecting browser settings.</span><br/><br/> <span style="color:#ffffff; background-color:#ffffff;">Please enable JavaScript in your browsers settings.</span><br/><br/> <span style="color:#ffffff; background-color:#ffffff;">--------------------------------------------------------------------------------------------------------</span><br/><br/> </div> </body> </html>
对于Phone1,WebServer返回的就是网页的实际数据。后续的HttpReqeust和HttpResponse只是获取该网页数据中某些图片的数据。
然而,对于Phone2,WebServer返回的是一段JavaScript的代码。通过分析不难看出,该代码用于在获取一些手机的参数,并将获取的参数拼接到一个新的URL里面,传到WebServer端。该网页JS的代码执行流程如下:
1) 网页解析进行到Body标签的时候,调用doLoad函数。
2) 在doLoad函数中调用setTimeout函数。
3) 在Timeout后调用setTimeout函数的第一个参数executeTest函数,进行获取device info和跳转URL的操作。
问题就出在第三步,由于当前Phone1和Phone2对JS的支持不完善。无法支持setTimeout JS函数。所以,导致无法跳转到网站的内容页面。所以,该页面body标签里的提示信息便显示了出来。
新问题:在测试的时候,当我们把Phone2的JavaScript Disable后,再去访问该网站,发现却能访问成功。这是什么原因呢?
原来在WebServer返回的这段代码中有这样一行:
<noscript> <meta http-equiv="refresh" content="1; url=http://wap.bild.de/?nbsrc=o2_w2g_textlink&emvcc=0&emvAD=240x320&nbcol=0%7Cunknown" /> </noscript>
noscript 元素用来定义在脚本未被执行时的替代内容(文本)。此标签可被用于可识别 <script> 标签但无法支持其中的脚本的浏览器。(详见:http://www.w3school.com.cn/tags/tag_noscript.asp)
那么,为什么WebServer会对Phone1和Phone2返回不同的数据呢?
同样我们抓出了Phone1和Phone2发给WebServer的Http Request信息:
Phone1:
Phone2:
通过对比发现,Phone1和Phone2的Http Request除了UA header和时间戳,其他参数都一样。所以,可以断定是UA Header导致了WebServer给Client端返回了不同的数据。
那么WebServer为什么需要这样实现呢?通过进行一步分析我们找到了WebServer的如下结构:
(更多:http://www.netbiscuits.com/solutions/platform/device-context-service/)
原来,WebServe会通过Client传过来的UA header到第三方的公司Netbiscuits提供的服务器上面获取该手机的Device information(例如:屏幕分辨率),用于优化网页的显示效果。对于Phone1来说,Netbiscuits的服务器中收录的该手机对应的信息,所以,WebServer能够获取到这些信息并对网页进行适配。所以,网页显示正常。然后对于Phone2,Netbiscuits服务器没有收录该手机的对应信息,所以,webserver试图通过Javascript函数获取这些信息。但是,由于Phone2的Browser无法支持JS的setTimeout函数。所以,这个问题便发生。