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