摘要:關于動靜分離的描述,這里推薦一篇不錯的博文網站靜態化處理動靜分離策略。這里的解決辦法則是采用的屬性,將其應用于數據請求相關的上,就可以達到腳本與數據并發加載的效果。
作者:莫冠釗
轉載請注明出處,保留原文鏈接和作者信息
前言當今許多大型網頁應用尤其是SPA均采用了動靜分離的策略。關于動靜分離的描述,這里推薦一篇不錯的博文 網站靜態化處理—動靜分離策略。
本人是做前端的,之前有幸與一位對性能追求極致的后端同學一起開發這種動靜分離的web項目,以下將從傳統順序模式、單路數據并發模式(以下簡稱單并發模式)、多路數據并發模式(以下簡稱多路并發模式)來談談自己對這類應用關于前端加載方面的心得。本文中的例子均來自該項目中。
1. 傳統順序模式一般情況下,瀏覽器首先會接收到一張靜態的頁面,這張頁面會包含樣式文件和腳本文件引用的標簽(圖片什么的不在這里討論)。至于數據哪里來,下面介紹兩種方式:
腳本請求獲取
通常,在腳本加載完畢后,腳本會執行一段向服務端發送請求數據的代碼,然后通過回調函數取出數據并做初始化工作。這一個過程為:請求頁面 => 渲染頁面 => 加載腳本 => 請求數據 => 數據與腳本一起初始化 => 初始化完畢,也就是從加載應用到啟動應用是以順序任務的形式執行。
直接填充于隱藏標簽中
服務端也可以直接將數據填充到網頁中的一個隱藏標簽中再傳回給客戶端,也就是上面順序中把獲取數據放在頁面請求之前。之后在腳本中直接去獲取相應的DOM中的內容也就是數據,來進行初始化工作。
這兩種方法各有優劣,因為不是本文重點,在此就直接帶過。不過筆者更傾向于前者。
1.1 工作流圖如果用工作流的思想去理解,大概可以為下圖(第一種方式):
1.2 結果分析在這里我們只研究數據以及main.js的加載情況。
base64.css是用來存儲一些小圖片的base64字符串并且是允許延后加載,可以將其歸為圖片資源一類。
總體情況還是可以接受的,畢竟后端同學對緩存這一塊下了很大的功夫,用戶會在500ms左右看到頁面的內容,到了600ms之后程序就可以正式啟動。
這種模式的優點是顯而易見的,這種順序加載啟動模式易用性、可維護性都比較好,也能很好地發揮動靜分離的特長。
然而,我們認為,如果將上圖中數據的請求放在前面和腳本一起并發請求,也許會減少整個頁面的加載和啟動所需時間,而且后端同學還覺得這樣的加載效果會更加直觀、整齊……
于是便有了下面的研究。
2. 單并發模式要實現數據與腳本并發加載,最核心的就是要讓數據不依賴于腳本進行加載,筆者所能想到的有兩種:
在頭部添加一個script,插入一段發送ajax請求的代碼,向服務端發送數據請求。
同樣是添加一個script,將其src設為數據請求的url來引用外部數據資源。
單從執行效率來說,1比2還多了一步,故本文中選擇2進行討論。
2.1難點與解決方案 如何保證script標簽進行外部下載時不阻塞其他資源的下載?把script在head標簽內。在下載script引入的外部腳本時,瀏覽器處于阻塞狀態,網絡不好或者script文件過大時,頁面處于空白停頓狀態,這樣的體驗是很不好的。
我們一般會將腳本文件放在頁面底部來降低腳本下載與運行所帶來的阻塞影響,而且這樣可以保證腳本中所引用的頁面元素已經渲染完畢。
而數據請求是與頁面元素無關,在這里我們希望它能放在頭部確??梢员M早地開始加載來達到與其它資源一起請求,但又不阻塞其他資源的下載。
瀏覽器對標記有async屬性的scripts會立即加載并解析,該script相對于頁面的其余部分異步地執行(當頁面繼續進行解析時,腳本將被執行)。
這里的解決辦法則是采用HTML5的async屬性,將其應用于數據請求相關的script上,就可以達到腳本與數據并發加載的效果。如下代碼:
script(src="/Table/Data" type="text/javascript" async="async")在數據與腳本加載的順序未知的情況下,如何保證正確的頁面啟動?
javascript是一門解析性語言,當它加載完畢之后就會執行。
此時的數據請求變成了一個script標簽,也就是說,它可以變成一段與賦值相關的javascript代碼,直接把得到的結果放在公共環境中。如果不把它變成賦值代碼,基于上面的引言,可能得到的數據就會變成環境中的一個匿名對象而在之后無法再次被訪問。這樣一來,在腳本記載完畢就可以直接去引用這個結果進行啟動頁面。那么問題來了……
基于上面async中闡述的方案,在實際中更多時候我們可能無法100%保證數據與腳本加載的先后順序。資源大小的確一定程度決定了加載時間,但是網絡傳輸也有著許多不穩定的因素。
我們也不可能直接在任何一個script中直接引用對方的資源(如果未加載完畢,會返回undefined的錯誤)。
不到萬不得已,不應該使用輪詢檢查的方法去解決并發問題,這樣的應用性能太低,和我們的初衷相違背。
既然它們是相互依賴的關系,而且我們只需要其中一方引用另一方的資源即可完成我們所需要的啟動。在這里,我們只需要讓先加載完成前的把資源暴露到公共環境window中,讓后加載的那一方察覺到之后直接引用進行啟動即可。
對于數據與腳本,我們把它們的資源分別定為:
名稱 | 資源 | 描述 |
---|---|---|
數據 | allData(Object) | 存儲所有的動態數據 |
腳本 | mainInitByData(Function) | 主引導函數 |
在數據請求里,代碼為:
var allData = window.allData = "{"name":"data"}"; //檢查腳本的資源是否存在 if (typeof window.mainInitByData !== "undefined") { mainInitByData(JSON.parse(allData)); };
腳本里相關的片段則為:
var mainInitByData = window.mainInitByData = function(data) { //TODO... } if (typeof window.allData !== "undefined") { mainInitByData(JSON.parse(allData)); }2.2 工作流圖 2.3 結果分析
不難發現,經過并行化處理之后,加載頁面的效率相比于之前的順序模式大大增加了。且頁面程序也能順利啟動(這里大家可以自行嘗試)。
不料后端同學在一兩個月后,又提出了希望作多路數據并發請求,因為動態數據中也有部分數據相對一段時間內為靜態的,這部分數據可以用緩存處理,其他數據則直接從其它服務器中獲取,可以進一步提高并發效率。事情變得越來越有趣,也有了下面的研究。
3. 多路并發模式 3.1 “繼承”單并發此時,假設我們所需請求的數據共有三條A、B、C,其中A為相對靜態數據,可以做出以下定義:
名稱 | 資源 | 描述 |
---|---|---|
子數據A | AData(Object) | 存儲A的相對靜態數據 |
子數據B | BData(Object) | 存儲B的動態數據 |
子數據C | CData(Object) | 存儲C的動態數據 |
腳本 | mainInitByData(Function) | 主引導函數 |
如果繼續沿用單并發中的策略,腳本的相關片段代碼則為:
var mainInitByData = window.mainInitByData = function(dataA, dataB, dataC) { //TODO... } if (typeof window.dataA !== "undefined" && window.dataB !== "undefined" && window.dataC !== "undefined") { var dataA = JSON.parse(dataA), dataB = JSON.parse(dataB), dataC = JSON.parse(dataC); mainInitByData(dataA, dataB, dataC); }
以上數據只是一個例子,并不代表這樣就可以解決這類的問題。假如有一天后端突然要求一次并發加載10條數據,代碼就會變得十分冗余。
既然要處理并發,那么單并發的思想是可以沿用的,只是這里的方向不對。
不妨我們換個角度思考,腳本仍然和數據進行互相檢查,但是這個數據包含了所有子數據,在這里我直接將其稱為父數據。那子數據之間怎么辦?
3.2 以信號量的思想處理數據整合之所以說是信號量的思想而不是信號量,因為信號量本身是多線程多任務同步,而對于帶有async標簽里的javascript是單線程異步,但不代表javascript不能利用信號量的思想,信號量的思想就是在解決處理并發問題。具體的信號量定義,請讀者自行查閱。
為了更好的描述這個借用思想的過程,先做以下定義:
父數據與子數據之間共用一種信號量,子數據運用這種信號量進行數據的整合,而父數據應用這種信號量進行與腳本初始化啟動。
每次子數據加載完畢后,釋放信號量,并把自己的數據整合到父數據中。
假設子數據之間申請信號量的順序未知,但必定在父數據之前。
整合的數據以及信號量都放在一個js對象integrateData中,分別命名為data、sem(其值為1-子數據數量),即integrateData = {data: {}, sem: -2}
這里可能需要對子數據的格式做一定的調整。變成以下類型,方便做整合
{"message":"success", "data": {....}}
那么對于所有子數據的處理代碼為:
var result = "JSON"; var integrateData = window.integrateData || (window.integrateData = { data: {}, sem: 1 - 3 }); var onDataCallback = window.onDataCallback || (window.onDataCallback = function(result_, integrateData) { function dataIsReady(integrateData) { return integrateData.sem > 0; } function dataReadyCallback(integrateData) { integrateData.sem--; //父數據與腳本啟動 var mainInitBydata = window.mainInitBydata; if (typeof mainInitBydata === "function") { mainInitBydata(integrateData); } integrateData.sem++; } if (dataIsReady(integrateData)) { alert("非法請求"); return; } var result = result_; if (typeof result_ === "string") { result = JSON.parse(result_); } //數據整合 if (result.message === "success") { var data = result.data; for (var key in data) { integrateData.data[key] = data[key]; } } //釋放信號量 integrateData.sem++; //檢查信號量 if (dataIsReady(integrateData)) { dataReadyCallback(integrateData); } }); onDataCallback(result, integrateData);
此時,腳本里的相關代碼則為:
var mainInitByData = window.mainInitByData = function(integrateData) { //TODO... } var integrateData = window.integrateData; //這里無需擔心沖突問題,因為js是單線程執行,子數據整合完畢后會直接執行父數據檢查腳本資源的行為,所以sem>0時,父數據處于就緒狀態。 if (integrateData && integrateData.sem > 0) { mainInitBydata(integrateData) }3.3 工作流圖 3.4 結果分析
其實效率相比單并發提高不多,主要是涉及的動態數據規模不大,而且每次發送的請求報文和響應報文都會有一定大小的報頭,造成不必要的開銷。但假如動態數據足夠大的話,這種策略是可以起到很大的作用。同時,單并發模式中的雙向檢查也可以用信號量的思想實現。
總結總結以上的模式,我們可以得出以下的結論:
模式 | 效率 | 易用性 | 性能主要影響因素 | 適用場景 |
---|---|---|---|---|
順序 | 普通 | 容易 | 數據與程序的大小總和 | 一般的小項目 |
單并發 | 比順序模式高 | 普通 | 數據與程序大小比例 | 大多數動靜分離的網站應用 |
多路并發 | 一般比單并發高,當數據太小時效率會比單并發低 | 復雜 | 劃分數據的比例 | 數據比較龐大的網站應用,尤其是數據之間按相對均勻的比例歸類 |
除此以外,上述中,單并發與多路并發的一大缺陷就是代碼的耦合性會相對地提高,對于多路并發而言,如果子數據請求之間有依賴關系,可能還要定義多種不同的信號量,不利于管理。
利用現有的工具比如EventProxy,可以很好管理這些并發請求,包括任務之間的依賴關系。通過事件訂閱與觸發的形式可以讓程序更好地知道當前所完成的任務以觸發相應的回調函數進行處理。
希望本文可以給讀者帶來一定的幫助。
最后打個小廣告,歡迎follow我的github:https://github.com/zero-mo
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87802.html
摘要:關于動靜分離的描述,這里推薦一篇不錯的博文網站靜態化處理動靜分離策略。這里的解決辦法則是采用的屬性,將其應用于數據請求相關的上,就可以達到腳本與數據并發加載的效果。 作者:莫冠釗 轉載請注明出處,保留原文鏈接和作者信息 前言 當今許多大型網頁應用尤其是SPA均采用了動靜分離的策略。關于動靜分離的描述,這里推薦一篇不錯的博文 網站靜態化處理—動靜分離策略。 本人是做前端的,之前有幸與一...
摘要:為了優化動靜混合站點和純動態站點的加速效果,阿里云推出了全站加速方案,通過智能區分動靜態請求,實現整站加速效果的全面提升。 摘要: 伴隨著近幾年O2O的爆發,網絡已經不僅是傳統的展示企業品牌的渠道,而逐漸演變成為嫁接企業和用戶之間服務和交流的橋梁,我們開始賦予網絡更多的功能,比如購物、出行、學習、娛樂等等。 同時,網絡內容形態的進階發展,網頁內容已經從靜態的圖片、文字向短視頻、直播演變...
摘要:接入層作用一的聚合。接入層作用二服務發現與動態負載均衡既然統一的入口變為了接入層,則接入層就有責任自動的發現后端拆分,聚合,擴容,縮容的服務集群,當后端服務有所變化的時候,能夠實現健康檢查和動態的負載均衡。 此文已由作者劉超授權網易云社區發布。 歡迎訪問網易云社區,了解更多網易技術產品運營經驗。 這個系列是微服務高并發設計,所以我們先從最外層的接入層入手,看都有什么樣的策略保證高并發。...
摘要:反向代理要說反向代理,我們就先要理解正向代理下面我們就談談正向代理和反向代理吧??蛻舳瞬拍苁褂谜虼?。反向代理總結就一句話代理端代理的是服務端。因此,動態資源轉發到服務器我們就使用到了前面講到的反向代理了。 反向代理 要說反向代理,我們就先要理解正向代理 ,下面我們就談談正向代理和反向代理吧。 正向代理 一個位于客戶端和原始服務器(origin server)之間的服務器,為了從原始...
閱讀 1702·2021-11-25 09:43
閱讀 2665·2019-08-30 15:53
閱讀 1808·2019-08-30 15:52
閱讀 2898·2019-08-29 13:56
閱讀 3317·2019-08-26 12:12
閱讀 565·2019-08-23 17:58
閱讀 2127·2019-08-23 16:59
閱讀 932·2019-08-23 16:21