逛京東的時候開著 chrome 控制檯,無意間看到了下面這串似曾相識的程式碼,
再看了下 localstorage,
看到這些內容,其實京東首頁的前端架構雛形就出來了。
JD 使用 seajs 作為模組載入器,使用 jd-jquery 為基本庫,看到它的 jq 版本是 1.6.4,
對比了下”正版” jquery-1.6.4 的原始碼,很顯然,JD 使用的是自己造的輪子,這說明京東的前端生態應該是十分完善的,有輪子就會有很多元件、外掛,這對一個公司批量造網頁很有裨益。
前後端情況
電商的主頁都是以呈現為主,展示各個橫向市場和縱向市場的入口,也可能會有一些個性化(所謂個性化,就是給不同的使用者推薦不同的內容)的推薦。它不像交易、詳情等頁面,頁面邏輯後端重而前端輕,後端需要做各種內容推薦、資料校驗、型別判斷等工作,而前端更多的是將後端的資訊有序地呈現出來。
首頁則不同,首頁承載了大量的二級頁面入口和一些頻道的推薦內容,而這些資料更多的是由運營去維護,可以認為資料是死的,就算存在一些個性化的資料,也會使用 jsonp 的形式去載入,前端需要快速高效地處理這些資料,可見前端任務相當艱鉅。加上作為一個網站的門面,它的安全穩定性也是極為重要的。
如果沒有猜錯,京東後端也有一箇中間層,中間層負責組裝資料,以模組為單位,根據前端的請求響應對應模組的內容,而資料是在另外一個運營平臺上維護,運營填好的資料會即時的推送到 CDN 或者應用,中間層拼合資料。看不到的東西就不猜測了,我們來看看京東首頁的整體結構。
前端技術簡要分析
如果希望一個網站跑起來飛快,你覺得怎麼做最靠譜?
我們都玩過微博,都上過手機淘寶,進入這些 app 應用,會發現很多頁面幾乎看不到載入的痕跡,因為他們是本地應用,很多圖片、指令碼、樣式都已經打包在本地了,所以載入起來速度是很快的。如果希望一個網頁也能飛奔起來,同樣的道理,讓請求的個數少一點,讓請求的內容少一點。還有一個至關重要的,讓那些次要的內容慢一點載入(我們稱之為懶載入)。
前端快取和非同步載入
京東在按需載入和資料快取上的工作做的十分到位。
每個具有 lazyload 非同步標識的模組,都包含兩個屬性,一個是渲染該模組需要的內容(資料+JS),一個是這個內容過期的時間,只要內容不變就不會過期,所以這裡使用的是檔案 hash 來標註。
把需要請求的路徑寫在 dom 上,使用者滾動時,一旦該模組進入了視窗,則請求 dom 上對應的 data-path 地址,拿到渲染這個模組所需要的指令碼和資料,不過這中間還有一層本地快取 localstorage,如果在本地快取中匹配到了對應的 hash string 內容,則直接渲染,否則請求到資料之後更新本地快取。dom 上的 data-time 會在頁面載入時候,後端計算檔案 hash,hash 不變則輸出內容也不變。
這裡其實存在兩個請求,一個請求是載入資料和指令碼,而這裡的內容是:
1 2 3 4 5 6 7 |
<div>{html}</div> <script> var data = {JSONSting}; seajs.use('path/to/$version$/script.js', function(Script){ Script.init(data); }); </script> |
為啥不在返回的內容中直接把指令碼也輸出出來?為了讓資料充分快取下了不少功夫。資料的變化頻率比較高,如果資料和初始化指令碼包裝在一起,雖然節約了一個請求,但一旦資料變化,整個指令碼都得重新載入,而將資料和指令碼分離,指令碼可以長期快取在本地,單獨請求資料,這個量會小很多。直接改變上面的 version
版本號便可以讓瀏覽器重新請求最新指令碼。
從上面可以看出,任何一個模組的改動,在前端只會引起一個較小的載入變化,加上 http 的快取策略,伺服器的壓力也是很小的。
工程結構
比較常見的工程結構,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
. ├── build/ └── src/ ├── widgets/ ├── mods/ | ├── moduleA/ | | ├── index.js | | ├── index.tpl | | └── index.less | ├── moduleB/ | └── moduleC/ ├── index.js ├── index.tpl └── index.less |
所有 mods 中的 tpl 檔案通過一些標籤,引入到 src/index.tpl 中,需要同步渲染的模組資訊直接引入,而非同步渲染的模組內容,比如 moduleA/index.tpl ,其內容就十分簡單:
1 2 3 4 |
<div lazyload data-path="<% moduleA %>" data-time="<% md5(moduleA + getData()) %>"></div> |
只引入一個模組鉤子(hook),然後按需載入/懶載入這個模組鉤子內容。相比 JD 採用的也是類似的模型。
橫向技術對比
看看上面列出的目錄結構,一般情況下,為了減少網頁的請求數,我們會把所有 mods 和 wedgets 中的 js 和 css 分別打包成一個檔案,然後前端 combo 請求,提前載入但是懶執行,這是 CMD 的思維方式。而京東使用了更懶的方式:懶載入並且懶執行。
這種方式帶來的好處就是,單個模組的更改,前端只更新一小部分快取;而提前載入所有模組的方式,任何一個模組有改動,整體都得重新下載。指令碼懶載入的缺點是,需要發起請求,如果需要載入多個模組,則需要發起多個指令碼請求,可以看到,快速拖動 JD 首頁,模組的載入速度不容樂觀。當然,指令碼是可以被瀏覽器快取的,這個問題也就是首次訪問或者清空了快取才會出現。
對請求控制如此嚴格,怎麼就沒考慮下優化原始碼當中的兩大段 css 和 js 程式碼呢?是不是也可以把 css 和 js 放到 localstorage 中,減少請求數。
原始碼中通過函式去載入資源:
1 2 3 4 |
var loadCss = function(){ var style = loadFromLocalstorage(); inserCss(style); }; |
如果 localstorage 中不存在,也不需要重新發請求,後端指令碼通過 cookie 判斷是否需要同步輸出程式碼:
1 2 3 4 |
// 虛擬碼 if(cookies('cssV') || cookie('cssV') !== 'setsV'){ echo CSSCode; } |
如果發現 cookie 中的版本號與設定的版本號不一樣,或者沒有 cssV cookie,則同步內聯輸出 css 和 js。
小結
本文只是對京東首頁用到的部分技術做一個簡要的分析,頁面載入速度確實十分可觀,贊!
隨著需求的多元化和終端裝置的多元化,前端技術在 web 舞臺上一直展現著優美的身姿,她在進化、在演變,幾乎每隔一兩個月就能聽到新的前端技術出來,所以學是學不過來的,前端的學習就兩個字:”理解為什麼”。