淺析天貓H5站點

葉小釵發表於2015-02-28

前言

我們做前端開發的時候,很有可能會做一個競品分析,比如我就做過去哪兒、藝龍、同程等與攜程的移動站點競品分析,競品分析的目的一般是技術對比,但是更多的是業務對比,知己知彼,百戰不殆;我們同時會借鑑、學習其它網站的技術,比如網站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 += '"\n     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 += '">\n    <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>\n</div>\n\n';
 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 = '\n<div id="ks-combobox-trigger-';
 30             var c = e(d, b, {}, "id", 0, 7);
 31             a += k(c, !0);
 32             a += '"\n     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 += '">\n    <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 + '">&#x25BC;</div>\n</div>\n'
 46         };
 47         a += j(d, f, b, "if", 6);
 48         a += '\n\n<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 += '">\n\n    <input id="ks-combobox-input-';
 56         b = e(d, f, {}, "id", 0, 15);
 57         a += k(b, !0);
 58         a += '"\n           aria-haspopup="true"\n           aria-autocomplete="list"\n           aria-haspopup="true"\n           role="autocomplete"\n           aria-expanded="false"\n\n    ';
 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 "\n    disabled\n    "
 66         };
 67         a += j(d, f, b, "if", 22);
 68         a += '\n\n    autocomplete="off"\n    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 += '"\n\n    value="';
 76         b = e(d, f, {}, "value", 0, 29);
 77         a += k(b, !0);
 78         a += '"\n    />\n\n\n    <label id="ks-combobox-placeholder-';
 79         b = e(d, f, {}, "id", 0, 33);
 80         a += k(b, !0);
 81         a += '"\n           for="ks-combobox-input-';
 82         b = e(d, f, {}, "id", 0, 34);
 83         a += k(b, !0);
 84         a += "\"\n            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 += ";'\n    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 += '">\n    ';
105         f = e(d, f, {}, "placeholder", 0, 37);
106         a += k(f, !0);
107         return a + "\n    </label>\n</div>\n"
108     }
109 });
View Code
 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 += '"\n 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 += "\n";
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 = "\n ";
24             a = i(g, a, {}, ".", 0, 4);
25             b += m(a, !0);
26             return b + "  \n"
27         };
28         c += 
29         k(g, f, e, "each", 3);
30         c += '\n"\n\n';
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 = " \n ";
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 + '"\n'
45         };
46         c += k(g, f, e, "each", 8);
47         c += '\n\nstyle="\n';
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 = " \n ";
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 + ";\n"
62         };
63         c += k(g, f, e, "each", 13);
64         return c + '\n">'
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:為什麼我這裡關注的這麼清楚呢,因為我這塊也沒做最近被業務團隊提了需求......

其次圖片輪播元件與下面用到的這個模組可以統一:

輪播元件繼承他稍作擴充套件即可,上面關注點基本聚焦到了一些細節上,再看看其它部分。

結語

今天的觀察還是過於細節化,停留在表面,加之家裡裝備不足,沒能將天貓的精髓看到,我們接下來幾天再觀察下,看看是否能觀察出天貓的效能處理方案,今天太晚了,暫時到此。

文中有何不足或者錯誤請您指正

相關文章