前言
我們做前端開發的時候,很有可能會做一個競品分析,比如我就做過去哪兒、藝龍、同程等與攜程的移動站點競品分析,競品分析的目的一般是技術對比,但是更多的是業務對比,知己知彼,百戰不殆;我們同時會借鑒、學習其它網站的技術,比如網站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 + '">▼</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 });
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:為什么我這里關注的這么清楚呢,因為我這塊也沒做最近被業務團隊提了需求......
其次圖片輪播組件與下面用到的這個模塊可以統一:
輪播組件繼承他稍作擴展即可,上面關注點基本聚焦到了一些細節上,再看看其它部分。
結語
今天的觀察還是過於細節化,停留在表面,加之家里裝備不足,沒能將天貓的精髓看到,我們接下來幾天再觀察下,看看是否能觀察出天貓的性能處理方案,今天太晚了,暫時到此。
文中有何不足或者錯誤請您指正