前一阵很辛劳,所以荒废了博客。前几天终于完成了这项艰苦卓绝的工程:HTML5版广告墙,决定写篇文章,记录一下踩过的坑。
项目介绍
广告墙属于典型的列表式应用:打开后是无尽列表,通过滑动手指驱使列表滚动,上拉加载更多内容,下拉刷新列表(这次没做)。单击列表中的某项,打开详细页;单击上面的后退按钮,退回上一级页面。
考虑到目标平台是Android系统,内嵌WebKit内核,我采用了时下流行的HTML5+CSS3技术,并且做好了向下兼容的准备。为了减少代码量,我放弃了jQuery,也没有考虑半调子Zepto,全部使用原生开发。操作上,开始想用Hammer.js封装手势,后来处理滚动效果处理得不好,最终选择了iScroll 5。模板方面,前端使用Handlebars.js,后端使用Mustache,所以编译时还写了段代码做转换。
项目代码托管在github上,因为前端不涉及到业务逻辑,所以仓库是公开的,有兴趣的同学以clone下来看看。
技术选型大概聊这些,接下来开始进入正题。
性能vs兼容性
实际上,随着硬件的发展,对于列表式这种非性能敏感性应用来说,性能早已不是制约其发展的首要因素了。另一方面,有一定专业精神的前端如我,平时也会注重积累各种性能优化的点,并不断在项目中实验、实施——所以我就很搞不明白,技术实力强大如jQuery,为啥会弄出个jQuery Mobile这种完全不能用的东西出来……
相反,在这个项目中,由于平台分裂带来的兼容性问题成了最大的绊脚石。尤其是这些兼容性问题,极少找到现成的解决方案……下面举两个例子:
Vivo X909 和z-index
正常情况下,position:absolute;
的dom节点在不显式设置z-index
属性区分层级的时候,默认后面的节点会显示在前面的节点上方。大多数浏览器里都是这样,可是Vivo X909就不行。
列表大家都知道,点击某项,详情会从右边滚动进来。由于没有设置-webkit-tap-highlight-color
(现在想想应该做的,下周回去加上吧),所以这个bug当时的的表现是“单击没有反应”。这种情况很难调试,我开始以为JS有错,试了很久,后来配置好Weinre(见上篇博客),才找到症结所在。最后把几个图层分别加上z-index
,问题解决。
早期Android的animation-fill-mode
我们仍有20%的用户在使用2.3.x版本的Android,所以这块市场必然不能放弃。在小米2上调是好的动画,在老HTC G10上,滑出的图层在动画结束后会消失。这次好在有了上次的经验,直接上Weinre;加上caniuse.com也有明确的描述,所以很快解决了。
各种难以定位、缺少文档、找不到解决方案的兼容性问题层出不穷,让我不禁感慨:本以为买上了WebKit的康庄大道,谁知尼玛这豆腐渣工程下面全是坑啊……
滚屏
前面提到,开始时间比较富裕,我也比较自信,想借助Hammer.js,慢慢打磨出色的滚动体验。后来无尽的bug环伺左右,实在无法专心研究,便退而求其次,使用iScroll了。
iScroll如今已经发展到5,其作者自信的表示:“我尽全力提升了其在Android平台上的表现,我相信现在它已经达到了极致。”实验效果确实很理想,无论是高配置的小米,还是老旧的HTC G10,都表现出色。将来有空的时候得好好研究下他是怎么做的。
工程实践
随着时代发展,Web开发这种脚本语言也开始分裂成开发、部署两个阶段,这是个好事情。核心代码需要为维护、测试留下足够的空间,势必在性能表现上有所不足,部署时根据应用场景的不同输出不同的结果正好能解决这个问题。这个项目中我使用Grunt,花费了不少精力在编写Gruntfile.js上。
开发时,我将测试用的数据,按照设计好的数据结构写在define.js中,生产环境中用服务器生成真实数据来替代。不同的应用场景,也用其配置。如今,目标支持的6个场景都能稳定工作,效果不错。
Windows下不好配ruby,使用SASS是个问题,不过后来找到了Prepros,问题迎刃而解。
技术细节分享
简单构造选择器
jQuery功能确实强大,不过实在太重型了;备胎Zepto轻便一些,不过功能也弱一些,语法做不到全兼容,用起来不舒服。仔细考虑这个项目的需求,无非是:找到某个元素,给它添加事件侦听;或者判断它是否包含某个class,那么自己写一个直接到Dom节点的选择器就足够了。后来看到篇文章,里面详细测试了querySelector
和getElementById
众的性能,发现还是后者大大领先——尤其在移动平台上,于是基本放弃了用前者的念头。
这个选择器实现起来很容易:
var $ = window.$ = function (selector, root) {
root = root || document;
var dom;
switch (selector.charAt(0)) {
case '#':
selector = selector.substr(1);
dom = document.getElementById(selector);
break;
case '.':
selector = selector.substr(1);
dom = root.getElementsByClassName(selector)[0];
break;
default :
dom = root.getElementsByTagName(selector)[0];
break;
}
return dom;
};
另外,$
还可以用来当命名空间,保存一些需要全局使用的变量和方法。
使用Handlebars.js生成预编译模板
预编译模版可以降低运行时的运算量,对于移动开发这种能省则省的应用场景,实在是必备之物。另一方面,为了方便开发,把模版放在HTML里更好调整。好在有“编译输出”这个步骤,可以两全其美。
Handlebars.js的预编译是把模版转换成根据数据生成HTML的JS代码,可以使用uglify压缩。编译之前最好过滤掉多余的空格和换行符,这样生成的JS文件也会小很多。
当然,再预编译也不如直接渲染HTML来的快,所以在服务器端生成页面的时候,一定要包含实际内容,让用户尽快得到信息。而PHP没有现成的Handlebars实现(我也不打算写),好在有mustache.php,并且handlebars兼容mustache(其实不完全,也被坑了),所以我写了一小段代码将Handlebars模版转换成Mustache模版。
使用attr()实现CSS中的多语言
除了传统的中文版,这次还要制作英文版。英文版与中文版的功能几乎完全一致,只是文字全部换成英文。最简单的做法就是搞个en.html——因为使用了模版,JS里不包含任何文字内容;但是“上拉加载更多”这个功能目前使用CSS的:after
实现——初衷是不修改dom,不引起relayout——当然,反正我用着Sass,增加个语言包不难,不过我还是希望把修改集中在一个地方。
后来查了下CSS表达式的兼容性,发现Android 2.1起就支持,那就好办了,先把各阶段文案放到HTML的属性里:
// 中文
<div id="list" data-normal="继续上拉,加载更多广告" data-more="松开加载" data-loading="加载中..." data-no-more="没有其它广告了,再看看吧" data-error="加载失败,请稍候重试" data-over=""></div>
// 英文
<div id="list" data-normal="Pull up to load" data-more="Release to load" data-loading="Loading..." data-no-more="No more ads" data-error="Load failed" data-over=""></div>
然后在CSS里使用attr()
访问即可
#list.over
&:after
content: attr(data-over)
#list.more
&:after
content: attr(data-more)
#list.loading
&:after
content: attr(data-loading)
#list.no-more
&:after
content: attr(data-no-more)
#list.error
&:after
content: attr(data-error)
WebKit中z-index
、translateZ(0)
混用,导致click
事件半失灵的解决
- 要在浏览器里触发下载,只要链接到一个浏览器打不开的文件就行。链接跳转是在
click
触发的浏览器内建行为,换言之,链接跳转依赖于click
的正常触发。 click
比touch event
晚200~300ms,所以为了保证用户体验,我们通常用tap
(touchstart
+touchend
)来替代click
响应用户操作- 所以一个“点击下载按钮”的操作,就会先响应
tap
事件,弹出详情浮层,告知用户获得积分的最终条件,然后等待浏览器开始下载 - CSS动画最好使用
translateZ(0)
促使浏览器启用GPU加速 - 这个bug在以上条件下产生,影响这个过程
之前我在PC版Chrome里也曾见识过这个bug(最新版没验证,大概20+的时候吧)。具体表现为,一个层,它的translateZ
有赋值,它可能被移到某处(通过translateX
和translateY
,或者动画过程中),不过还未到达那里,但是它仍然会拦截那里mousedown
/mouseup
事件,导致click
事件无法正常触发。
这个bug非常难排查,因为它并不影响tap
;视觉上看不到那个层,会想当然的认为不应该跟那个层有关系;并且,WTF,如果按得时间稍长,比如半秒,就可以触发click
……因为触发条件比较复杂,我甚至很长时间没找到规律。最后只好请出“小黄鸭调试法”,试图向索隆解释发生了什么,然后发现无法正常下载的按钮都是被详情浮层遮盖着,继而回忆起以前解决过类似问题。
最后解决这个问题的方案我其实不很满意,不过确实能顺利工作:
- 层的当前位置不会覆盖按钮,先不给层增加动画
tap
后,给click
留出足够的时间(我这里用400ms),再给层添加动画- 确保
click
可以响应,通过setTimeout
给层添加动画——本来300ms就测试通过,不过为了保险,我还是决定写成400
仍然悬而未决的问题
虽说项目已经快上线了,但仍有几个不解之谜……
- 某些手机仍然无法下载(启动
<a>
) - 快速拖动的时候可能把内容拖走(应该是触发了浏览器默认行为)
总结
做完项目回头看,系统分裂造成的兼容性问题比想象中多很多,原先以为最多是有些2.x版本不支持,做向下兼容就好,谁知还有各种实现细节问题。不过,在PC Chrome + 手机Chrome + Weinre + alert
之后,只要有耐心,大多数问题时可以解决的。
说完开发效率,另一方面再来看看产品表现。我们对性能不需要太乐观,也不用太悲观,敏感型应用,老老实实用原生开发,或编译成原生应用;非敏感型,跨平台高兼容性(咦,这眼泪哪里来的)的优势很明显。通过我实际看到移动端的表现,我们这些页面仔应该更有信心,将来移动大市场里,Web App的份额绝对不比Native App要少。
这项事业任重而道远,需要我们一起努力。
欢迎吐槽,共同进步