簡介詳情頁也叫做單品頁,域名以「item.jd.com/skuid.html」為格式的頁面,。是負責(zé)展示京東商品SKU的落地頁面,。主要任務(wù)是展示商品相關(guān)信息,如價格,、促銷、庫存,、推薦,從而引導(dǎo)用戶進入購買流程,。同時單品頁有很多版本,。一般分為兩類。一類我們通??吹降摹竿ㄓ妙惸吭斍轫摗埂蓄惸慷伎梢允褂茫活愂遣唤?jīng)??吹降摹复怪睂傩栽斍轫摗埂恍┯刑厥鈱傩缘纳唐芳?/p> 首先,,由于詳情頁量大(SKU數(shù)十億),、高并發(fā)(日PV數(shù)十億)等特性,在很長的一段時間里,,單品頁都是后端程序生成靜態(tài)頁面使用CDN來解決量大,、高并發(fā)的問題。 其次,。單品頁涉及的「三方」系統(tǒng)特別多,,比如促銷,、庫存,、合約、秒殺,、預(yù)售、推薦,、IM、店鋪,、評價社區(qū)等,。而單品頁的主要任務(wù)就是展示這些系統(tǒng)的信息,,并且適當(dāng)?shù)奶幚硭麄冎g的邏輯關(guān)系,而這些系統(tǒng)的接口一般都使用異步Ajax來完成,,因為其一CDN無法做到頁面的動態(tài)化,其二一些系統(tǒng)的信息對實時性要求特別高(價格,、秒殺),,即使使用后端動態(tài)渲染也很難做到無緩存零延遲。 基于上面兩個原因,,注定了單品頁是一種重多系統(tǒng)業(yè)務(wù)邏輯展示型頁面,重前端頁面,。我大概匯總了一下頁面上異步接口,總共約有30個,,頁首屏的接口特別重要,,接口之間幾乎都有耦合關(guān)系: 前端的發(fā)展歷程混沌時期 混沌時期的單品頁并沒有前端開發(fā)的概念。核心的功能腳本只有三個:促銷價格(promotion.js)、庫存地區(qū)(iplocation.js),、其它邏輯(pshow.js),。這三個腳本分別是三個不同團隊的同事負責(zé)維護,當(dāng)時我剛進入京東的時候在UED部門,,負責(zé)頁面腳本整體的維護工作和pshow的開發(fā)。那時候我自己維護的pshow.js腳本壓縮后只有80kb,,所有的代碼都是過程式的,沒有任何使用模式和代碼技巧,,JS最多也只被用來做個判斷渲染DOM,。那時候的前端工作內(nèi)容只在UI層面,,寫樣式和一些交互腳本。 這個階段給我最深刻的感覺是單品頁后端模板很少維護(后端架構(gòu)是最老的aspx版本),。大多數(shù)的改動都要用JavaScript去動態(tài)渲染。因為后端頁面是一個生成器生成的,。如果頁面后端模板有改動那么就需要全量的生成一次,過程可能需要幾個小時,。 初見端倪 當(dāng)我接手這個項目時剛好有一次大改版,,就在這時候老大說頁面上的腳本都要放在我們手里維護,。然后就是一大波的重構(gòu),、重寫?;旧蟨show被重寫了大概80%其它的因為業(yè)務(wù)邏輯的問題并沒有完全重寫,,只是做了些代碼層面的優(yōu)化。 有一個模板引擎叫trimPath,,知道這個的估計都算老前端了。最早的客戶端JavaScript MVC模式代表作品,,只到現(xiàn)在還在使用。這個階段像評價這種完全異步加載的模塊特別適合使用模板引擎來減少維護的工作量,。這個時候雖然頁面上的代碼并不都是我們寫的,但基本上前端對頁面的JavaScript有了控制權(quán),,接下來的事情就是尋找機會逐個優(yōu)化。 這段時間是最痛苦的時候,,維護的工作統(tǒng)一到前端,。然后后端幾乎沒有變化,,只是在一段時間將后臺的架構(gòu)從aspx過渡到了java,。本質(zhì)上并沒有什么改變,。前端卻做了比以前更多的事情,也是在這個時候我接手了大量的維護工作(包含全站公共庫的維護)使得我意識到了一些自動化,、工程化方面的重要性,,后文會主要講解,順便說下,,那時候前端自動化工具Grunt剛面世,但是我自己卻用的是apache ant,,不過不久就切換到了Grunt來構(gòu)建項目。 撥云見日 單品頁不僅重系統(tǒng)邏輯,,也重維護,。在這段時間里一方面有正常的維護類需求要做,,一方面自己也不斷的學(xué)習(xí)新知識為以后的改版做鋪墊。不過就在這時單品頁有歷史意義的一次技改出現(xiàn)了——單品頁動態(tài)化技改,。關(guān)于后端部分的改造細節(jié)可以去億級商品詳情頁架構(gòu)演進技術(shù)解密了解。 總的來說這次的改版后很多數(shù)據(jù)直接從后端讀取,,不再從前端異步獲取而且我們也做過一些異步加載的優(yōu)化,多接口combo從統(tǒng)一服務(wù)吐出給前端使用,。這時前端就不用再為異步接口的加載時苦腦了,,只需要專注系統(tǒng)接口的邏輯。 隨著這次技改,,前端的代碼也迎來了模塊化的時代,。我們把所有的前端代碼都進行了模塊化然后基于SeaJS重寫,,配合Nginx concat功能實現(xiàn)了本地模塊化開發(fā),線上服務(wù)端合并,。 單品頁前端模塊的結(jié)構(gòu)與劃分概覽 上圖可以看出,,基本上最核心的模塊都在首屏,。每個模塊都有單獨的一/多個腳本,。代碼行數(shù)(LOC)由230+~1200+不等。通常來說代碼行數(shù)越多代碼復(fù)雜性就越高,,邏輯越復(fù)雜。很難想象「購買方式」這種只有一行屬性選擇功能的代碼行數(shù)卻高達1200多行,。其主要原因就在于購買方式所在的系統(tǒng)和其它首屏核心系統(tǒng)(庫存、促銷,、地址選擇,、白條)都有邏輯上的耦合。 看著不錯,,然而在一個前端工程師眼里至少應(yīng)該是這樣的(我只取了一些典型的模塊,,并不是全部): 這就可以解釋為什么有的時候只是加一個很小的東西我們都為考慮再三然后通過AB測試提取相關(guān)數(shù)據(jù),最后后再進行決策,。單品頁的首屏可以說是寸土寸金,。 按什么維度劃分模塊 起初我按模塊的屬性劃分,比如核心,、公共腳本、模塊腳本,。但用了一段時候以后發(fā)現(xiàn)這樣劃分在單品頁這種大型系統(tǒng)中并不科學(xué),因為這樣劃分出來的代碼只有劃分的人知道是什么規(guī)則,,其它人接手代碼很難快速掌握代碼架構(gòu),而且尤其在模塊比較多的時候不方便維護,。 后來我嘗試完全以功能模塊在頁面上出現(xiàn)的位置維度劃分,。這樣以來維護起來方便多了,,需要修改某個模塊代碼只需要對照著圖里面標(biāo)識的模塊信息就能輕易找到代碼。 整體核心模塊 我們按頁面上的模塊結(jié)構(gòu)首屏劃分出來這幾個核心模塊:
項目的整體樹形結(jié)構(gòu)是這樣的: 模塊內(nèi)部結(jié)構(gòu) 比如下面這個大圖預(yù)覽的功能,,我全部放在一個文件夾里面維護,但是邏輯上的JavaScript模塊是分離的,,只是說文件夾(preview)就代表頁面上的某一部分功能集合: 注意文件夾的命名有一定的規(guī)則:
我們再來看下自動生成生成的__sprite.scss是什么內(nèi)容:
注意引用的mixin名稱和我們需要手動添加的樣式類名一致,。當(dāng)然也可以直接生成一個類名對應(yīng)的樣式,但是靈活性不好,。比如hover的時候是另外一張圖片就沒法自動生成了。 前端技能樹一,、HTMLDOM節(jié)點數(shù) 與重業(yè)務(wù)邏輯的頁面不同,重展示的頁面一般具有很高的DOM節(jié)點數(shù),。比如京東首頁,,正常情況加載完頁面一共有3500多個DOM節(jié)點,基本上全部用于展示商品信息,、廣告圖和內(nèi)容布局,頁面上的三方異步服務(wù)也比較少,。尤其像頻道頁基本上沒有什么業(yè)務(wù)上的邏輯,全部是靜態(tài)頁面,。這種頁面的特點是更新?lián)Q代頻率高,,一年兩三次改版很正常,,CMS做模塊化后兩天換個皮膚都是沒問題的,。但是這種思路并不適合單品頁。單品頁更重業(yè)務(wù)邏輯,,同時展示層UI邏輯也有很多關(guān)系,。 我自己的經(jīng)驗是:頁面上的DOM節(jié)點數(shù)絕對不能超過5000個,否則頁面滾動的時候就會出現(xiàn)卡頓的情況,,尤其是移動端,。 同步渲染還是異步加載 理論情況下最好的做法是后端同步動態(tài)渲染頁面,但是由于Web應(yīng)用中很多功能都是用戶行為驅(qū)動的,。同步加載不可避免的消耗了后端服務(wù)資源。比如非首屏模塊(公共頭尾,、評價),、點擊事件觸發(fā)的DOM內(nèi)容(異步tab)。 所以我的經(jīng)驗是:能放到后端做判斷渲染的DOM就盡量放在后端(尤其是首屏),。這樣做的好處有四點好處:
對于異步渲染的模塊來說,后端通常需要判斷「頁面有什么元素」,,以及元素之間的依賴對應(yīng)關(guān)系;而前端需要專注于「元素應(yīng)該怎么展示」,,UI層面的交互以及模塊與模塊之前的邏輯關(guān)系,。 其實更多的時候異步是一種沒有辦法的辦法,也就是說異步是其它方案都解決不了的情況下才考慮的,。 外鏈靜態(tài)資源 盡量使用外鏈CSS和JavaScript資源,,一方面便于緩存,減少服務(wù)同步輸出的資源浪費,。IE 6里面會有一些可怪的bug,,比如有內(nèi)聯(lián)樣式style標(biāo)簽的頁面A如果在另外一個頁面B中的link標(biāo)簽中引用,那么這段style會在B頁面也起作用,。 使用雙協(xié)議的URL 使用//來代替http:和https:瀏覽器會自動適應(yīng)兩種協(xié)議的資源訪問,,兼容性較好。注意IE 8下使用腳本更新src為雙協(xié)議時會出現(xiàn)bug,,建議使用location.protocol來判斷然后做兼容處理,。 刪除元素默認屬性 比如script標(biāo)簽?zāi)J的type就是text/javascript,如果script里面的內(nèi)容是JavaScript時可以不用寫type,。另外如果要在頁面里面插入一段不需要瀏覽器解析的HTML片段時可以將type寫成text/x-template(任意不存在的type)用于放置模板文件,,通常用來在腳本中獲取其innerHTML而無任何負作用。 給腳本控制元素加上類鉤子 在腳本中取頁面元素使用J-前綴類名,,與普通樣式類分離,。這樣做會生成很多冗余的類名,但卻很好的降低了樣式和腳本的耦合,,并且在重構(gòu)和腳本職位分開團隊里會是一條最佳實踐。 二、CSS樣式分類 所有頁面只共享一個sass Mixin,,里面包含了基礎(chǔ)的sass語法糖,、常用類(清浮動,、頁面整體顏色字體等),。 模塊級的樣式分為兩類:
雪碧圖 關(guān)于雪碧圖我經(jīng)驗是:永遠不要想把所有的圖標(biāo)拼合在一起。按模塊而不是按頁面去拼sprite更合理,,更方便維護,,然后配合構(gòu)建工具自動接合生成樣式文件才是最好的解決方案,。當(dāng)然如果你的頁面比較簡單,那這條規(guī)則并不適用,。說到這個問題我就得把珍藏多年的圖片拿出來show一把,,用事實來說明為什么把所有圖片都拼在一張圖上就一定是對的。早期由于年輕篤信將所有的icon拼在一張圖上才是完美的(圖 1) 后來維護起來實在不方便,,就把按鈕全部單獨接合起來,。注意,當(dāng)時的按鈕都是圖片,,設(shè)計方面要求的很嚴格,。加入購物車按鈕做的也非常漂亮(圖 2) 然后這些都不是最典型的,下面這個promise icon才是(圖 3) 從圖里面可以看到,,這個功能在第一個版本的時候只有7個icon,,后來不斷增加,最多的時候達到77個,。以至于當(dāng)時每周都會添加兩個的頻率,。 同時這個icon當(dāng)時接合的時候技術(shù)上也有問題:不應(yīng)該把文字也切到圖片里面,主要原因是早期icon比較少加上外邊框樣式對齊的問題綜合選擇了直接使用圖片,。 后來我就覺得這樣是不對的,。然后通過和產(chǎn)品的溝通,說明我的考慮以及新的解決方案后得到了認同,。結(jié)果就是對圖片不進行拼合,,后臺上傳經(jīng)過審核的不帶文字icon,文字由接口輸出,,然后在產(chǎn)品上做了約定:icon最多不能超過4個,,代碼里也做了相應(yīng)限制。這樣就能保證頁面上的請求數(shù)不會太多同時方便系統(tǒng)維護,,問題得到了解決,。 適當(dāng)使用DataURI 這個在一些小圖片場景方面特別適合,比如1*1的占位圖,、loading圖等,,不過IE 6并不支持這種寫法,需要的時候可以加上一些兼容寫法:
關(guān)于兼容性 兼容性可以說是前端工程師在平常開發(fā)中花費很大量無意義工作的地方,。關(guān)于兼容性我想說的是如果你不愿意去說服周圍的人放棄或者讓他們意識到兼容性是個不可能完全解決的問題,,那么你就得為那些低級瀏覽器給你帶來的痛苦埋單。 其實更好的辦法是你和設(shè)計,、產(chǎn)品溝通然后給出一種分級支持的方案,。把每種瀏覽器定義一個級別。然后在開發(fā)功能的時候以「漸進增強」的方式。通常來講我們的解決方案是在低級瀏覽器里面保證流程正常進行,、模塊可以使用,,但忽略一些無關(guān)緊要的錯位、不透明等問題,,在高級瀏覽器里面需要對設(shè)計稿進行精確還原,,適當(dāng)?shù)募由弦恍┚咸砘ㄔ诩毠?jié)。比如微小的動畫,、邏輯細節(jié)上的處理等,。 舉個例子吧,下面這個進度條表示預(yù)約的人數(shù),,它是接口異步加載完才展示的,。如果加載完就立即設(shè)置進度條寬度會顯得生硬無趣,但是如果加上一點動畫效果的話就好多了,。然而問題又來了,,如果加上動畫那么邏輯上這個進度條應(yīng)該是一點點的增加,對應(yīng)的人數(shù)也應(yīng)該是逐個增加,。于是我就做了個優(yōu)化,,讓人數(shù)在這段時間內(nèi)均勻的增加。這個細節(jié)并不是很容易被人發(fā)現(xiàn),,但是這種設(shè)計會讓用戶感覺很用心而且有意思: 三,、JavaScript單品頁的腳本加載/執(zhí)行順序:
入口腳本 大致代碼如下:
注意模塊路徑中的MOD_ROOT是提前在頁面定義好的一個seajs path。目的是為了把前端版本號更新的控制權(quán)釋放給后端,,從而解決了前后端依賴上線不同步造成的緩存延遲問題,,配置腳本中只有幾個定義好的路徑:
還有一點,在測試環(huán)境的頁面中版本號(上面代碼中的1.0.12是一個全量的版本號)是后端從URL上動態(tài)讀取的(使用參數(shù)訪問就可以命中對應(yīng)版本item.jd.com/sku.html?version=1.0.12),。這樣以來測試環(huán)境上就可以并行測試不同版本的需求,,而且互不影響。當(dāng)然如果不同版本的后端代碼也有修改的話這樣是不行的,因為后端代碼也需要有個對應(yīng)的版本號,。 不過我們已經(jīng)解決了這個問題,。后端會在測試環(huán)境里動態(tài)加載后端模板并且可以做到版本號與前端一致。這樣以來配合git方便的分支策略就可以同時并行開發(fā)測試多個需求,,不用單獨配多個測試環(huán)境,。什么?你還在使用SVN,!哦,。那當(dāng)我沒說過。 事件處理模型 客戶端的JavaScript代碼基本上都是事件驅(qū)動的,,代碼的加載解析依賴于瀏覽器提供的DOM事件,。比如onload、mouseover,、scroll等,。 事件驅(qū)動的的模型特別適用于異步編程,而JavaScript天生就是異步,,所有的異步操作行為都最終會在一個回調(diào)函數(shù)(callback)中觸發(fā),。 比如單品頁中價格接口,加載完成后需要更新DOM元素來展示實時價格,;地區(qū)選擇接口加載完成后會更新配送信息,、庫存/商品狀態(tài)等,偽代碼如下:
上面的兩段代碼分別在兩個腳本中維護,,因為他們的邏輯相對獨立,。早期并沒有關(guān)聯(lián)關(guān)系。后來需求有變,,他們之間需要共享一些對方的數(shù)據(jù)(切換地區(qū)后需要重新獲取價格數(shù)據(jù)并展示),。但是物理上又不能放在一起通過使用全局變量的方式共享,而且它們都是異步加載接口后才取到數(shù)據(jù)的,,并不好確定誰先誰后(非要做到那就只能用全局變量雙向判斷),。所以這樣并不能很好的解決問題,而且代碼的耦合度會成倍增加,。 這時候我們引入了一種設(shè)計模式來解決這種問題——發(fā)布者/訂閱者,,我們把這種模式抽象成了自定義事件代碼來解決這一問題。這段代碼是由YUI核心開發(fā)者NicholasC. Zakas實現(xiàn)的,。代碼很簡單,,事件對象主要有兩個方法addListener(type,listener)和fire(event),。于是我們重構(gòu)了上面的偽代碼:
需要注意的一點是,,必須確保事件先注冊后觸發(fā)執(zhí)行,也就是說先addListener,,再fire,。 一些典型的性能優(yōu)化點 基本上客戶端的JavaScript性能問題都來自于DOM查找和遍歷,在使用的時候一定要小心,,可能不經(jīng)意的一個操作就會損失很多性能,,尤其在低端瀏覽器中。順便多說一點,,現(xiàn)代的JavaScript解釋器本身是很快的,,語言層面的性能問題很少遇到。DOM查找慢是因為瀏覽器給JavaScript訪問頁面提供的一套DOM API本身慢:
前端工程化原由 前端工程化其實并不是最近兩年才有的概念,。大約在2013年的時候Grunt問世的時候就已經(jīng)有所涉及。這類打包工具主要的目的是自動化一些開發(fā)流程,,我最早使用Grunt來構(gòu)建代碼的時候只解決了三個問題:
當(dāng)時我還在組內(nèi)做過一個分享,,有興趣的可以去圍觀一下Best WorkflowWith Grunt。 其實這些工具出現(xiàn)的原因是:當(dāng)時前端領(lǐng)域的各種基礎(chǔ)設(shè)施很缺乏,,而前端的工作內(nèi)容又相對零散,。工作時需要開很多的軟件。再加上JavaScript語言本身也很弱,,就連包管理這種基礎(chǔ)的東西也沒有內(nèi)置,,以至于模塊化要通過一些第三方類庫來實現(xiàn),比如:RequireJS,、SesJS,。 工具的重要性可以在我之前的一個分享中找到前端開發(fā)工具系列。 現(xiàn)狀 如今前端工程的生態(tài)環(huán)境由于NodeJS的出現(xiàn)已經(jīng)變得很好了,。你可以根據(jù)自己的需求選一個合適的直接用到項目里面,。像Grunt、Gulp,、browserify,、webpack等。不過要明白這些工具的出現(xiàn)從另一方面證明了前端開發(fā)天生存在很多的問題:
這些問題幾乎都是歷史性的原因和兼容性因素造成的。作為一名好的前端工程師要看清楚現(xiàn)狀,,然后按自己項目的需求去定制一些前端工程化的方案,,而不是隨波逐流。 選擇 其實現(xiàn)在自己開發(fā)一套前端工程化/自動化流程的成本已經(jīng)很低了,,你只需要學(xué)習(xí)一些NodeJS的知識,,配合NPM包管理機制,隨手就搞出一個構(gòu)建工具出來,。因為并不需要你去實現(xiàn)什么東西,,所有的東西都有現(xiàn)成的包。腳本壓縮有UglifyJS,、CSS優(yōu)化有CSS-min,、圖片壓縮優(yōu)化有PNG-quant等。你只需要想清楚自己要達到什么目的,,解決什么問題就可以抄家伙自己寫一套工作流出來,。 我自己的經(jīng)歷也從Grunt、GulpJS到現(xiàn)在自造輪子,。自己根據(jù)需求開發(fā)出來一套集成的打包工具,,有興趣的可以去圍觀一下Wooo。 當(dāng)然你也可以不用任何打包工具,,自己寫一些NPM Script來完全定制化項目開發(fā)/測試/打包流程,。我猜這也是為什么現(xiàn)在類似Grunt不再那么火,Gulp遲遲沒有發(fā)布4.0版本的原因,。寫一個構(gòu)建工具的成本太低了,,而且這種集成的工具很難滿足差異的開發(fā)需求。君不知已有人意識到了這一點么why-i-left-gulp-and-grunt-for-npm-scripts,。 程序,、設(shè)計、產(chǎn)品我始終認為程序,、設(shè)計是為了產(chǎn)品服務(wù)的,。好的產(chǎn)品是要重視設(shè)計的,好的(前端)工程師是要有一些審美素養(yǎng),。 其實很多時候技術(shù)解決方案都是要根據(jù)產(chǎn)品的定位來設(shè)計的,,了解產(chǎn)品需求以后才能定制出真正合適的高效的解決方案。好比前面講到的那個sprite案例,,如果一開始就和產(chǎn)品討論好方案后來也不可能有那種失控的情況發(fā)生,。在產(chǎn)品形成/上線前期能發(fā)現(xiàn)問題比上線后發(fā)現(xiàn)問題更容易解決。 這部分內(nèi)容和代碼無關(guān),,就不多說了,。然而早年我還有一次分享關(guān)于前端、改變,。 總結(jié)關(guān)于單品頁的前端開發(fā)本篇文章只是冰山一角,,還有很多沒有提及,每個小東西都可以單獨寫一篇文章來分享,。隨后希望可以有更多的總結(jié)和分享,。 相關(guān)閱讀: |
|