前言

我们做前端开发的时候,很有可能会做一个竞品分析,比如我就做过去哪儿、艺龙、同程等与携程的移动站点竞品分析,竞品分析的目的一般是技术对比,但是更多的是业务对比,知己知彼,百战不殆;我们同时会借鉴、学习其它网站的技术,比如网站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是阿里的一套前端框架,里面有很多组件和工具类,可惜我还没来得急拜读,这里只能瞎子摸象了。

 a.on("click tap",
function(a) {
i.show();
e.later(function() {
i.addClass("category-dialog-unfold")
},
10);
t.fadeIn(.2)
})

可以看到这个框架里面应该封装了类似jQuery/Zepto之类的dom库,这里如此的绑定了事件,再深入我们不管,但是我认为这里还是直接使用fastclick来的好,编码时候便不用写tap这类事件模拟了。

这里获得的第二个信息是,天猫团队是采用了模块加载的,同样也是依赖kissy的:

 KISSY.add("fp-m/mods/category", function(e, a, r) {
var i = e.one("#J_CategoryDialog");
var t = e.one("#J_CategoryMask");
var n = {init: function() {
var e = this;
e._initCategoryTrigger();
e._initCategoryClose()
},_initCategoryTrigger: function() {
var a = e.one("#J_CategoryTrigger");
if (!i || !t || !a)
return;
a.on("click tap", function(a) {
i.show();
e.later(function() {
i.addClass("category-dialog-unfold")
}, 10);
t.fadeIn(.2)
})
},_initCategoryClose: function() {
var a = e.one("#J_CategoryClose");
if (!i || !t || !a)
return;
a.on("click tap", function(e) {
e.halt();
i.removeClass("category-dialog-unfold");
t.fadeOut(.2)
})
},_loginHandler: function() {
}};
return n
}

虽然并未使用kissy,但是一套框架完成这么多事情,我觉得是不是kissy对于移动端来说可能有点笨重,这个问题的答案是:

第二个应该是kissy的核心库,感觉还行,具体还得深入了解kissy才行,这里不多说,其中一段代码我非常感兴趣:

 KISSY.add("combobox/combobox-xtpl", [], function() {
return function(f) {
var a, d = this;
a = this.config.utils;
var j = a.runBlockCommand, k = a.renderOutput, g = a.getProperty, h = a.runInlineCommand, e = a.getPropertyOrRunCommand;
a = '<div id="ks-combobox-invalid-el-';
var b = e(d, f, {}, "id", 0, 1);
a += k(b, !0);
a += '"\n class="';
var b = {}, c = [];
c.push("invalid-el");
b.params = c;
b = h(d, f, b, "getBaseCssClasses", 2);
a += k(b, !0);
a += '">\n <div class="';
b = {};
c = [];
c.push("invalid-inner");
b.params = c;
b = h(d, f, b, "getBaseCssClasses", 3);
a += k(b,
!0);
a += '"></div>\n</div>\n\n';
var b = {}, c = [], m = g(d, f, "hasTrigger", 0, 6);
c.push(m);
b.params = c;
b.fn = function(b) {
var a;
a = '\n<div id="ks-combobox-trigger-';
var c = e(d, b, {}, "id", 0, 7);
a += k(c, !0);
a += '"\n class="';
var c = {}, g = [];
g.push("trigger");
c.params = g;
c = h(d, b, c, "getBaseCssClasses", 8);
a += k(c, !0);
a += '">\n <div class="';
c = {};
g = [];
g.push("trigger-inner");
c.params = g;
b = h(d, b, c, "getBaseCssClasses", 9);
a += k(b, !0);
return a + '">▼</div>\n</div>\n'
};
a += j(d, f, b, "if", 6);
a += '\n\n<div class="';
b = {};
c = [];
c.push("input-wrap");
b.params = c;
b = h(d, f, b, "getBaseCssClasses", 13);
a += k(b, !0);
a += '">\n\n <input id="ks-combobox-input-';
b = e(d, f, {}, "id", 0, 15);
a += k(b, !0);
a += '"\n aria-haspopup="true"\n aria-autocomplete="list"\n aria-haspopup="true"\n role="autocomplete"\n aria-expanded="false"\n\n ';
b = {};
c = [];
m = g(d, f, "disabled", 0, 22);
c.push(m);
b.params = c;
b.fn = function() {
return "\n disabled\n "
};
a += j(d, f, b, "if", 22);
a += '\n\n autocomplete="off"\n class="';
b = {};
c = [];
c.push("input");
b.params = c;
b = h(d, f, b, "getBaseCssClasses", 27);
a += k(b, !0);
a += '"\n\n value="';
b = e(d, f, {}, "value", 0, 29);
a += k(b, !0);
a += '"\n />\n\n\n <label id="ks-combobox-placeholder-';
b = e(d, f, {}, "id", 0, 33);
a += k(b, !0);
a += '"\n for="ks-combobox-input-';
b = e(d, f, {}, "id", 0, 34);
a += k(b, !0);
a += "\"\n style='display:";
b = {};
c = [];
g = g(d, f, "value", 0, 35);
c.push(g);
b.params = c;
b.fn = function() {
return "none"
};
b.inverse = function() {
return "block"
};
a += j(d, f, b, "if", 35);
a += ";'\n class=\"";
j = {};
g = [];
g.push("placeholder");
j.params = g;
j = h(d, f, j, "getBaseCssClasses", 36);
a += k(j, !0);
a += '">\n ';
f = e(d, f, {}, "placeholder", 0, 37);
a += k(f, !0);
return a + "\n </label>\n</div>\n"
}
});
 KISSY.add("component/control/render-xtpl", [], function() {
return function(f) {
var c, g = this;
c = this.config.utils;
var k = c.runBlockCommand, m = c.renderOutput, h = c.getProperty, e = c.runInlineCommand, i = c.getPropertyOrRunCommand;
c = '<div id="';
var d = i(g, f, {}, "id", 0, 1);
c += m(d, !0);
c += '"\n class="';
var d = {}, n = [];
n.push("");
d.params = n;
e = e(g, f, d, "getBaseCssClasses", 2);
c += m(e, !0);
c += "\n";
e = {};
d = [];
n = h(g, f, "elCls", 0, 3);
d.push(n);
e.params = d;
e.fn = function(a) {
var b;
b = "\n ";
a = i(g, a, {}, ".", 0, 4);
b += m(a, !0);
return b + " \n"
};
c +=
k(g, f, e, "each", 3);
c += '\n"\n\n';
e = {};
d = [];
n = h(g, f, "elAttrs", 0, 8);
d.push(n);
e.params = d;
e.fn = function(a) {
var b;
b = " \n ";
var c = i(g, a, {}, "xindex", 0, 9);
b += m(c, !0);
b += '="';
a = i(g, a, {}, ".", 0, 9);
b += m(a, !0);
return b + '"\n'
};
c += k(g, f, e, "each", 8);
c += '\n\nstyle="\n';
e = {};
d = [];
h = h(g, f, "elStyle", 0, 13);
d.push(h);
e.params = d;
e.fn = function(a) {
var b;
b = " \n ";
var c = i(g, a, {}, "xindex", 0, 14);
b += m(c, !0);
b += ":";
a = i(g, a, {}, ".", 0, 14);
b += m(a, !0);
return b + ";\n"
};
c += k(g, f, e, "each", 13);
return c + '\n">'
}
});

可以看到,这个应该是html模块化的东西,以underscore的模板引擎来说是这样的:

<div><span>我是:</span><%=name%></div>
 var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='<div><span>我是:</span>'+
((__t=(name))==null?'':__t)+
'</div>';
}
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:;
z-index:;
}

② 侧边栏

③ 侧边栏隶属的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>
 _initCategoryClose: function() {
var a = e.one("#J_CategoryClose");
if (!i || !t || !a) return;
a.on("click tap",
function(e) {
e.halt();
i.removeClass("category-dialog-unfold");
t.fadeOut(.2)
})
}

可以看到,这里的事件绑定依旧在采用on、bind之类的做法,其实这种方式应该摒弃,每个模块都可以看成一个组件,在模块show后,统一将事件点代理到根元素,比如这样:

events: {
'click selector': function() {}
//......
}

这样的话,效果好得多,不必显示的时候绑定事件,消失的时候移除事件什么的。

另外一个体验上的问题是,这个侧边栏我觉得应该采用局部滚动方式fixed布局,采用类似IScroll类方案,体验可能会更好,这里点击蒙版关闭组件的操作也应该有。

这块有点太细了,我们再看看其它地方,比如非常常用的图片轮播组件。

图片轮播

天猫的图片轮播组件,采用的也是transform的方式做移动,传统的是采用移动left,这种方式基本被摒弃。

transform: translate(-1600px, 0px)

全站的图片都是做了延迟加载的,但是就图片轮播组件这里的延迟加载却让我有点不理解了,请看dom结构:

可以看到,他是图片滑到对应index索引位置才动态的将img标签插入进去,而上面的导航一致在重绘,如果网络比较慢的话就会出现这种情况:

对的,因为节点已经生成,出来了一个白屏的项目,其实这里可以加上延迟加载那个图标的,便不会出现白屏。

PS:为什么我这里关注的这么清楚呢,因为我这块也没做最近被业务团队提了需求......

其次图片轮播组件与下面用到的这个模块可以统一:

轮播组件继承他稍作扩展即可,上面关注点基本聚焦到了一些细节上,再看看其它部分。

结语

今天的观察还是过于细节化,停留在表面,加之家里装备不足,没能将天猫的精髓看到,我们接下来几天再观察下,看看是否能观察出天猫的性能处理方案,今天太晚了,暂时到此。

文中有何不足或者错误请您指正

浅析天猫H5站点的更多相关文章

  1. 【web前端面试题整理08】说说最近几次面试(水)

    为什么换工作 换工作简单来讲一般会归纳为钱不够或者人不对,我们团队氛围很不错,所以基本就定位到钱不够了,而我更多是考虑到以后的职业发展,简单说来就是对以后几年的工作有想法,而这种想法实现不一定能在现在 ...

  2. 【前端优化之渲染优化】大屏android手机动画丢帧的背后

    前言 上周我与阿里的宇果有一次技术的交流,然后对天猫H5站点做了一些浅层次的分析,后面点时间基本天天都会有联系,中途聊了一些技术细节.聊了双方团队在干什么,最后聊到了前端优化.因为我本身参与了几次携程 ...

  3. 【移动前端开发实践】从无到有(统计、请求、MVC、模块化)H5开发须知

    前言 不知不觉来百度已有半年之久,这半年是996的半年,是孤军奋战的半年,是跌跌撞撞的半年,一个字:真的是累死人啦! 我所进入的团队相当于公司内部创业团队,人员基本全部是新招的,最初开发时连数据库都没 ...

  4. 用H5开发微信还是开发APP?

    用H5开发微信还是开发APP? 随着技术的飞速发展,HTML第五版技术标准的更新,在移动端,由于其相对较低的开发成本及强大的跨平台运行能力,越来越多的信息型产品也开始选择这样轻量级的H5页面进行快速迭 ...

  5. 浅谈Hybrid技术的设计与实现

    前言 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 浅谈Hybrid技术的设计与实现第三弹——落地篇 随着移动浪潮的兴起,各种APP层出不穷,极速的业务扩展提升了团队对开发 ...

  6. 【blade利刃出鞘】一起进入移动端webapp开发吧

    前言 在移动浪潮袭来的时候,小钗有幸进入框架组做webapp框架开发,过程中遇到了移动端的各种坑,也产生了各种激情,就我们公司的发展历程来说 第一阶段:使用传统方式开发移动站点,少量引入HTML5元素 ...

  7. 【初探IONIC】不会Native可不可以开发APP?

    前言 Hybrid技术流行已经有一段日子了,楼主的关注点也一直围绕着移动端围绕着Hybrid相关展开,Hybrid已经是大大提升开发效率的开发方式了,但是仍然需要至少一个IOS与Andriod,那么可 ...

  8. 谈一谈前端多容器(多webview平台)处理方案

    文中是我个人的一些开发经验,希望对各位有用,也希望各位多多支持讨论,指出文中不足以及提出您的一些建议. 双容器 得益于近几年移动端的发展,前端早已今非昔比,从大型框架来说angularJS.react ...

  9. 【大前端之前后分离01】JS前端渲染VS服务器端渲染

    前言 之前看了一篇文章:@Charlie.Zheng Web系统开发构架再思考-前后端的完全分离,文中论述了为何要前后分离,站在前端的角度来看,是很有必要的:但是如何说服团队使用前端渲染方案却是一个现 ...

随机推荐

  1. php安装threads多线程扩展

    php5.3或以上,且为线程安全版本.apache和php使用的编译器必须一致.通过phpinfo()查看Thread Safety为enabled则为线程安全版.通过phpinfo()查看Compi ...

  2. WebAPI

    WebAPI的Host OWIN IIS WebAPI 的handler和Filter有啥区别? WebAPI  常用 Filters Exception Filter Timer Filter Lo ...

  3. XSS 前端防火墙 —— 天衣无缝的防护

    上一篇讲解了钩子程序的攻防实战,并实现了一套对框架页的监控方案,将防护作用到所有子页面. 到目前为止,我们防护的深度已经差不多,但广度还有所欠缺. 例如,我们的属性钩子只考虑了 setAttribut ...

  4. AutoMapper(七)

    返回总目录 Null值替换 如果源类型的成员链上的属性值为Null,Null值替换允许提供一个可替换的值.下面有两个类Person和PersonInfo类,都有一个属性Title(头衔),从Perso ...

  5. [DeviceOne开发]-do_LinearLayout组件使用技巧

    一.摘要 今天跟大家主要来介绍一下DeviceOne中的do_linearlayout这个布局,在DeviceOne开发中,do_linearlayout这个布局是相当重要. 现在给大家来总结一下. ...

  6. Linux C++ 开发简介

    主要介绍将Windows程序迁移到Linux系统相关知识 简介 Windows程序迁移到Linux系统可能需要修改很多代码, 既需要了解Linux平台的开发知识, 也需要了解Windows平台代码如何 ...

  7. 基于Quick-cocos2d-x的资源更新方案 二

    写在前面 又是12点半了,对于一个程序员来说,这是一个黄金时间,精力旺盛,我想,是最适合整理和分享一些思路的时候了. 自从上次写了 基于Quick-cocos2d-x的资源更新方案 同样可见quick ...

  8. ASP.NET MVC 描述类型(一)

    ASP.NET MVC 描述类型(一) 前言 在前面的好多篇幅中都有提到过ControllerDescriptor类型,并且在ASP.NET MVC 过滤器(一)篇幅中简单的描述过,今天我们就来讲一下 ...

  9. JavaScript的基准测试-不服跑个分?

    原文:Bulletproof JavaScript benchmarks 做JavaScript的基准测试并没有想的那么简单.即使不考虑浏览器差异所带来的影响,也有很多难点-或者说陷阱需要面对. 这是 ...

  10. Go语言实战 - revel框架教程之缓存和Job

    所有的网站应该都会有一个非常简单的需求,首页一秒之内打开. 满足的方式主要有两种: 页面静态化,效果最好,对服务器基本没负担,只要带宽足够就好了.我知道一个PV过亿的站点就是全站静态(以前新浪也是), ...