前言
我们做前端开发的时候,很有可能会做一个竞品分析,比如我就做过去哪儿、艺龙、同程等与携程的移动站点竞品分析,竞品分析的目的一般是技术对比,但是更多的是业务对比,知己知彼,百战不殆;我们同时会借鉴、学习其它网站的技术,比如网站HTML使用、class命名、使用了什么新技术,还有优化体验相关的,对大型网站的学习分析是对自己网站提高的借鉴,也是个人能力的提升途径,今天我们就来一起学习下天猫的移动站点。
PS:此文单独学习借鉴,不涉及其它,请相关同事不要在意,文中有误请提出。
打开站点首页http://www.tmall.com/,一个站点映入眼帘:
一般情况下网站加载很快,文档加载结束在200ms左右,我们看一个网站首先会看他是否遵循web标准,所谓web标准不是那么绝对,简单来说HTML、JS、CSS各干各的,并且不要犯一些低级错误,比如标签闭合、标签名小写什么的,但是当我进入第二个页面却发现一个不好的地方。
DOCTYPE不顶行
当我点击天猫精选与品牌墙时,发现其中的源文件有一个问题:
可以看到,这个doctype没有顶行,我为什么会关注这个呢,因为携程现在的站点是采用的.net,.net会在cshtml第一行写一个using XXX之类的服务器端脚本,我们在grunt打包的时候没有压缩,然后一个页面的表现十分怪异,header里面一部分html代码跑到了下面,我开始以为是有标签没有闭合,或者有标签嵌套错误导致,调了好久才发现是doctype没顶行写,这个时候页面会按照怪异模式解析,导致了莫名其妙的问题。
再看天猫超市频道:
可能因为是php的,导致页面生成的有点怪,但是这些问题应该在发布时候做html压缩。
SEO相关
从源文件与最后生成dom来说,天猫不太注重SEO,这个是阿里与百度角力所致,这种不做SEO的站点尤其适合做webapp,但是我们看到天猫依旧采用的多页的模式,可能是出于成本或者webapp不成熟考虑吧。
由于这里没有SEO需求,我们不在这里多做纠缠,但是我注意到了另外一个问题:
移动站点未使用section、header等html5标签,是因为要考虑低版本兼容,或者没有seo需求觉得这样做意义不大呢?这个不可预知。
300ms延迟
一般移动站点会有300ms延迟问题,我特地去试了点击一个按钮,响应十分迅速,这个一般是两种解决方案:
① fastclick
② tap
我们跟进一个按钮试试看,比如这个分类按钮:
<a href="javascript:void(0);" target="_self" id="J_CategoryTrigger" class="category-trigger">分类</a>
从这个点击其实可以看到一些天猫团队的素质,就简单说下这个J_CategoryTrigger钩子,因为我是做单页应用的,所以一般会将事件钩子放到class里面,这里放到id里面的,其实要移植到class里面也相当容易,这样的意义是dom结构可能变化,但是我钩子却是不变的,这个对前端样式升级会提供好处,这里扯的有点远,我们继续深入这个按钮,最后在这里发现了调用点:
这里我们还意外收获到一个信息,天猫是依赖与kissy的,kissy是阿里的一套前端框架,里面有很多组件和工具类,可惜我还没来得急拜读,这里只能瞎子摸象了。
1 a.on("click tap", 2 function(a) { 3 i.show(); 4 e.later(function() { 5 i.addClass("category-dialog-unfold") 6 }, 7 10); 8 t.fadeIn(.2) 9 })
可以看到这个框架里面应该封装了类似jQuery/Zepto之类的dom库,这里如此的绑定了事件,再深入我们不管,但是我认为这里还是直接使用fastclick来的好,编码时候便不用写tap这类事件模拟了。
这里获得的第二个信息是,天猫团队是采用了模块加载的,同样也是依赖kissy的:
1 KISSY.add("fp-m/mods/category", function(e, a, r) { 2 var i = e.one("#J_CategoryDialog"); 3 var t = e.one("#J_CategoryMask"); 4 var n = {init: function() { 5 var e = this; 6 e._initCategoryTrigger(); 7 e._initCategoryClose() 8 },_initCategoryTrigger: function() { 9 var a = e.one("#J_CategoryTrigger"); 10 if (!i || !t || !a) 11 return; 12 a.on("click tap", function(a) { 13 i.show(); 14 e.later(function() { 15 i.addClass("category-dialog-unfold") 16 }, 10); 17 t.fadeIn(.2) 18 }) 19 },_initCategoryClose: function() { 20 var a = e.one("#J_CategoryClose"); 21 if (!i || !t || !a) 22 return; 23 a.on("click tap", function(e) { 24 e.halt(); 25 i.removeClass("category-dialog-unfold"); 26 t.fadeOut(.2) 27 }) 28 },_loginHandler: function() { 29 }}; 30 return n 31 }
虽然并未使用kissy,但是一套框架完成这么多事情,我觉得是不是kissy对于移动端来说可能有点笨重,这个问题的答案是:
第二个应该是kissy的核心库,感觉还行,具体还得深入了解kissy才行,这里不多说,其中一段代码我非常感兴趣:
1 KISSY.add("combobox/combobox-xtpl", [], function() { 2 return function(f) { 3 var a, d = this; 4 a = this.config.utils; 5 var j = a.runBlockCommand, k = a.renderOutput, g = a.getProperty, h = a.runInlineCommand, e = a.getPropertyOrRunCommand; 6 a = '<div id="ks-combobox-invalid-el-'; 7 var b = e(d, f, {}, "id", 0, 1); 8 a += k(b, !0); 9 a += '" class="'; 10 var b = {}, c = []; 11 c.push("invalid-el"); 12 b.params = c; 13 b = h(d, f, b, "getBaseCssClasses", 2); 14 a += k(b, !0); 15 a += '"> <div class="'; 16 b = {}; 17 c = []; 18 c.push("invalid-inner"); 19 b.params = c; 20 b = h(d, f, b, "getBaseCssClasses", 3); 21 a += k(b, 22 !0); 23 a += '"></div> </div> '; 24 var b = {}, c = [], m = g(d, f, "hasTrigger", 0, 6); 25 c.push(m); 26 b.params = c; 27 b.fn = function(b) { 28 var a; 29 a = ' <div id="ks-combobox-trigger-'; 30 var c = e(d, b, {}, "id", 0, 7); 31 a += k(c, !0); 32 a += '" class="'; 33 var c = {}, g = []; 34 g.push("trigger"); 35 c.params = g; 36 c = h(d, b, c, "getBaseCssClasses", 8); 37 a += k(c, !0); 38 a += '"> <div class="'; 39 c = {}; 40 g = []; 41 g.push("trigger-inner"); 42 c.params = g; 43 b = h(d, b, c, "getBaseCssClasses", 9); 44 a += k(b, !0); 45 return a + '">▼</div> </div> ' 46 }; 47 a += j(d, f, b, "if", 6); 48 a += ' <div class="'; 49 b = {}; 50 c = []; 51 c.push("input-wrap"); 52 b.params = c; 53 b = h(d, f, b, "getBaseCssClasses", 13); 54 a += k(b, !0); 55 a += '"> <input id="ks-combobox-input-'; 56 b = e(d, f, {}, "id", 0, 15); 57 a += k(b, !0); 58 a += '" aria-haspopup="true" aria-autocomplete="list" aria-haspopup="true" role="autocomplete" aria-expanded="false" '; 59 b = {}; 60 c = []; 61 m = g(d, f, "disabled", 0, 22); 62 c.push(m); 63 b.params = c; 64 b.fn = function() { 65 return " disabled " 66 }; 67 a += j(d, f, b, "if", 22); 68 a += ' autocomplete="off" class="'; 69 b = {}; 70 c = []; 71 c.push("input"); 72 b.params = c; 73 b = h(d, f, b, "getBaseCssClasses", 27); 74 a += k(b, !0); 75 a += '" value="'; 76 b = e(d, f, {}, "value", 0, 29); 77 a += k(b, !0); 78 a += '" /> <label id="ks-combobox-placeholder-'; 79 b = e(d, f, {}, "id", 0, 33); 80 a += k(b, !0); 81 a += '" for="ks-combobox-input-'; 82 b = e(d, f, {}, "id", 0, 34); 83 a += k(b, !0); 84 a += "" style='display:"; 85 b = {}; 86 c = []; 87 g = g(d, f, "value", 0, 35); 88 c.push(g); 89 b.params = c; 90 b.fn = function() { 91 return "none" 92 }; 93 b.inverse = function() { 94 return "block" 95 }; 96 a += j(d, f, b, "if", 35); 97 a += ";' class=""; 98 j = {}; 99 g = []; 100 g.push("placeholder"); 101 j.params = g; 102 j = h(d, f, j, "getBaseCssClasses", 36); 103 a += k(j, !0); 104 a += '"> '; 105 f = e(d, f, {}, "placeholder", 0, 37); 106 a += k(f, !0); 107 return a + " </label> </div> " 108 } 109 });
1 KISSY.add("component/control/render-xtpl", [], function() { 2 return function(f) { 3 var c, g = this; 4 c = this.config.utils; 5 var k = c.runBlockCommand, m = c.renderOutput, h = c.getProperty, e = c.runInlineCommand, i = c.getPropertyOrRunCommand; 6 c = '<div id="'; 7 var d = i(g, f, {}, "id", 0, 1); 8 c += m(d, !0); 9 c += '" class="'; 10 var d = {}, n = []; 11 n.push(""); 12 d.params = n; 13 e = e(g, f, d, "getBaseCssClasses", 2); 14 c += m(e, !0); 15 c += " "; 16 e = {}; 17 d = []; 18 n = h(g, f, "elCls", 0, 3); 19 d.push(n); 20 e.params = d; 21 e.fn = function(a) { 22 var b; 23 b = " "; 24 a = i(g, a, {}, ".", 0, 4); 25 b += m(a, !0); 26 return b + " " 27 }; 28 c += 29 k(g, f, e, "each", 3); 30 c += ' " '; 31 e = {}; 32 d = []; 33 n = h(g, f, "elAttrs", 0, 8); 34 d.push(n); 35 e.params = d; 36 e.fn = function(a) { 37 var b; 38 b = " "; 39 var c = i(g, a, {}, "xindex", 0, 9); 40 b += m(c, !0); 41 b += '="'; 42 a = i(g, a, {}, ".", 0, 9); 43 b += m(a, !0); 44 return b + '" ' 45 }; 46 c += k(g, f, e, "each", 8); 47 c += ' style=" '; 48 e = {}; 49 d = []; 50 h = h(g, f, "elStyle", 0, 13); 51 d.push(h); 52 e.params = d; 53 e.fn = function(a) { 54 var b; 55 b = " "; 56 var c = i(g, a, {}, "xindex", 0, 14); 57 b += m(c, !0); 58 b += ":"; 59 a = i(g, a, {}, ".", 0, 14); 60 b += m(a, !0); 61 return b + "; " 62 }; 63 c += k(g, f, e, "each", 13); 64 return c + ' ">' 65 } 66 });
可以看到,这个应该是html模块化的东西,以underscore的模板引擎来说是这样的:
<div><span>我是:</span><%=name%></div>
1 var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; 2 with(obj||{}){ 3 __p+='<div><span>我是:</span>'+ 4 ((__t=(name))==null?'':__t)+ 5 '</div>'; 6 } 7 return __p;
我们会有一个预编译操作,将对应的模块文件转为下面这种AMD规范模式,这样做可能会使体积有一丝丝的增加,但是却可以绕过一次javascript编译,对手机的执行效率以及电池的耗损都有好处,而这一工作一般是配合grunt在发布前完成的。
但是,天猫或者说kissy的做法,由于代码是压缩的,我有点看不出深浅,希望不是在拼接字符串吧。
PS:这里说300ms延迟扯得有点远。
层级关系
一般来说一个站点的z-index应该由js开发与css同时设计,但是阿里的规则是必须同时get javascript与css两项技能,所以这个zindex可能是自己规划的,首先这里的图片轮播导航条跑到了侧边栏上面:
这里视觉上脱离文档流的元素有:
① 图片轮播导航
.slide_1425130247393-631fader-nav-div { display: inline-block; position: absolute; bottom: 6px; left: 12px; padding: 0; z-index: 10; }
② 侧边栏
③ 侧边栏隶属的mask蒙版
④ 最下面的导航条
但是真实场景与我预料的却大不一样,他的导航条是relative的,然后里面的元素全部是absolute的......
说实话,因为我不是专业的CSS,这里有点看不出深浅,但是relative的话,我要是页面有resize操作,可能要出问题,比如:
最后统计站点的几个关键z-index值:
① 轮播图片导航白点 absolute zIndex:10
② 侧边栏包裹层 relative zIndex:4
③ 侧边栏内部元素 absolute zIndex:100
④ 广告栏 fixed zIndex:9999
这个在zIndex应该是没有规划的,我再看看后面一个页面的弹出层:
absolute,zIndex为100
absolute 在zIndex:9999
经过观察我得到一个结论,天猫全站弹出层z-index未做规划,这个在多页应用中问题不大,但是一旦采用webapp模式或者伪单页模式,弹出层一多便容易出问题,戒之慎之。
规范化事件
天猫站点我觉得另外一个有问题的地方是,事件未被统一化,比如上面的弹出层弹出后有一个关闭按钮,那么他的事件绑定在哪呢?
这里的dom结构是:
<a href="javascript:void(0);" id="J_CategoryClose" class="category-close" target="_self" data-spm-anchor-id="875.7403452.0.0">关闭</a>
1 _initCategoryClose: function() { 2 var a = e.one("#J_CategoryClose"); 3 if (!i || !t || !a) return; 4 a.on("click tap", 5 function(e) { 6 e.halt(); 7 i.removeClass("category-dialog-unfold"); 8 t.fadeOut(.2) 9 }) 10 }
可以看到,这里的事件绑定依旧在采用on、bind之类的做法,其实这种方式应该摒弃,每个模块都可以看成一个组件,在模块show后,统一将事件点代理到根元素,比如这样:
events: { 'click selector': function() {}
//...... }
这样的话,效果好得多,不必显示的时候绑定事件,消失的时候移除事件什么的。
另外一个体验上的问题是,这个侧边栏我觉得应该采用局部滚动方式fixed布局,采用类似IScroll类方案,体验可能会更好,这里点击蒙版关闭组件的操作也应该有。
这块有点太细了,我们再看看其它地方,比如非常常用的图片轮播组件。
图片轮播
天猫的图片轮播组件,采用的也是transform的方式做移动,传统的是采用移动left,这种方式基本被摒弃。
transform: translate(-1600px, 0px)
全站的图片都是做了延迟加载的,但是就图片轮播组件这里的延迟加载却让我有点不理解了,请看dom结构:
可以看到,他是图片滑到对应index索引位置才动态的将img标签插入进去,而上面的导航一致在重绘,如果网络比较慢的话就会出现这种情况:
对的,因为节点已经生成,出来了一个白屏的项目,其实这里可以加上延迟加载那个图标的,便不会出现白屏。
PS:为什么我这里关注的这么清楚呢,因为我这块也没做最近被业务团队提了需求......
其次图片轮播组件与下面用到的这个模块可以统一:
轮播组件继承他稍作扩展即可,上面关注点基本聚焦到了一些细节上,再看看其它部分。
结语
今天的观察还是过于细节化,停留在表面,加之家里装备不足,没能将天猫的精髓看到,我们接下来几天再观察下,看看是否能观察出天猫的性能处理方案,今天太晚了,暂时到此。
文中有何不足或者错误请您指正