動態web網站的歷史能夠追溯到萬維網初期,相比於靜態網站,動態網站提供了強大的可交互功能.通過幾十年的發展,動態網站在互動性和頁面顯示效果上有了很大的提高,可是對於網站動態網站的總體頁面加載架構沒有作太大的改變.對於用戶而言,頁面的加載速度極大的影響着用戶體驗感.與靜態網站不一樣,除了頁面的傳輸加載時間外,動態網站還需考慮服務端數據的處理時間.像facebook這樣大型的用戶社交網站,必須考慮用戶訪問速度問題,javascript
傳統web模式採用了順序處理的流程來處理用戶請求.即用戶向客戶端發送一個請求後,服務器處理請求,加載數據並渲染頁面;最後將頁面返回給客戶端.整個過程是串行執行的,具體流程以下:css
1. 瀏覽器發送一個HTTP請求到Web服務器。
2. Web服務器解析請求,而後讀取數據存儲層,制定一個HTML文件,並用一個HTTP響應把它發送到客戶端。
3. HTTP響應經過互聯網傳送到瀏覽器。
4. 瀏覽器解析Web服務器的響應,使用HTML文件構建了一個的DOM樹,而且下載引用的CSS和JavaScript文件。
5. CSS資源下載後,瀏覽器解析它們,並將它們應用到DOM樹。
6. JavaScript資源下載後,瀏覽器解析並執行它們。html
整個流程按必須順序執行,不能重疊,這也是爲何傳統模式隨着網絡速度的提高訪問速度沒有很大提高的緣由.解決順序執行的速度問題,通常能想到的就是多線程併發執行.Facebook 的前端性能研究小組採用瀏覽器和服務端併發執行的思路,通過了六個月的努力,開發出了BigPipe頁面異步加載技術,成功的將我的空間主頁面加載耗時由原來的5 秒減小爲如今的2.5 秒。這就是咱們本文要介紹的高性能頁面加載技術---BigPipe.前端
BigPipe的主要思想是實現瀏覽器和服務器的併發執行,實現頁面的異步加載從而提升頁面加載速度.爲了達到這個目的,BigPipe首先根據頁面的功能或位置將一個頁面分紅若干模塊(模塊稱做pagelet),並對這幾個模塊進行標識.舉個例子,在博客園我的首頁包括幾大板塊,如頭部信息,左邊信息,博文列表,footer等.咱們能夠將首頁按這些功能分塊,並用惟一id或名稱標識pagelet.客戶端向服務端發送請求後(發出一次訪問請求,如請求訪問我的博客首頁),服務端採用併發形式獲取各個pagelet的數據並渲染pagelet的頁面效果.一旦某個pagelet頁面渲染完成則馬上採用json形式將該pagelet頁面顯示結果返回給客戶端.客戶端瀏覽器會根據pagelet的id或標識符,在頁面的制定區域對pagelet進行轉載渲染.客戶端的模塊加載採用js技術.具體流程以下:java
1. 請求解析:Web服務器解析和完整性檢查的HTTP請求。
2. 數據獲取:Web服務器從存儲層獲取數據。
3. 標記生成:Web服務器生成的響應的HTML標記。
4. 網絡傳輸:響應從Web服務器傳送到瀏覽器。
5. CSS的下載:瀏覽器下載網頁的CSS的要求。
6. DOM樹結構和CSS樣式:瀏覽器構造的DOM文檔樹,而後應用它的CSS規則。
7. JavaScript中下載:瀏覽器下載網頁中JavaScript引用的資源。
8. JavaScript執行:瀏覽器的網頁執行JavaScript代碼python
前三個階段由Web服務器執行,最後四個階段(5,6,7,8)是由瀏覽器執行。因此在服務器能夠採用多線程併發方式對每一個pagelet進行數據獲取和標記生成頁面,生成好的pagelet頁面發送給前端.同時在瀏覽器端,對css,js的下載能夠採用並行化處理.這就達到了瀏覽器和服務器的併發執行的效果,這樣使得多個流程能夠疊加執行,加少了總體頁面的加載時間.瀏覽器端的並行化交給瀏覽器本身處理不須要咱們作額外工做.在BigPipe中主要是處理服務端的並行性.git
在BigPipe,一個用戶請求的生命週期是這樣的:在瀏覽器發送一個HTTP請求到Web服務器。在收到的HTTP請求,並在上面進行一些全面的檢查, 網站服務器當即發回一個未關閉的HTML文件,其中包括一個HTML 標籤和標籤的開始標籤。標籤包括BigPipe的JavaScript庫來解析Pagelet之後收到的答覆。在標籤,有一個模板,它指定了頁面的邏輯結構和Pagelets佔位符。例如:github
1 <html> 2 <head> 3 <title>struts2-bigpipe-plugin</title> 4 <script type="application/javascript"> 5 function replace(id, content) { 6 var pagelet = document.getElementById(id); 7 pagelet.innerHTML = content; 8 } 9 </script> 10 </head> 11 <body> 12 <table width="100%"> 13 <tr border="1"> 14 <td width="50%" bgcolor="#f0f8ff"><div id="one">${one}</div></td> 15 <td width="50%" bgcolor="#faebd7"><div id="two">${two}</div></td> 16 </tr> 17 <tr> 18 <td width="50%" bgcolor="#7fffd4"><div id="three">${three}</div></td> 19 <td width="50%" bgcolor="#8a2be2"><div id="four">${four}</div></td> 20 </tr> 21 </table>
注意:這個html沒有以</body> </html>結束,這是一個未關閉的頁面模板(這裏定義爲index.ftl,是freemarker模板).若是是封閉的頁面,那麼瀏覽器就不會等待也不接收服務器以後返回的數據.因此這裏必須設置爲未關閉的頁面.web
服務端返回給客戶端頁面模板(index.ftl)後,並行的處理各個pagelet的數據並和將數據填補pagelet對應的頁面顯示模板(以下面代碼的one.ftl)中獲得一個渲染完成的頁面.若是某個pagelet的頁面渲染完成,那麼就將pagelet的id或標示符,html內容及相關數據組成json數據馬上發送給客戶端.客戶端根據pagelet的id和html內容,以及以前傳回來的模板(index.ftl),利用模板(index.ftl)中的JavaScript解析函數將pagelet的數據加載到頁面對應的位置.下面是對應上面模板中id爲one的pagelet的頁面模板(one.ftl):spring
<h1>Part One</h1> <h2>你好:${user.name},如今時間時 ${time} </h2>
服務端將數據填充到one.ftl頁面後,將構建json數據,並當即將json數據發送給客戶端(瀏覽器).下面是服務器發回給客戶端的pagelet one對應的數據.爲了使解析的js書寫簡單(index.ftl的js解析方法replace()),這裏沒有直接轉換爲json,而是用html數據返回給前端:
<script type="application/javascript\"> replace("one", "<h1>Part One</h1> <h2>你好:John,如今時間時 23:20 </h2>" ); </script>
若是用json表達的話,通常是如下格式,可是須要將index.ftl的replace()函數改寫:
<script type=」text/javascript」> replace( {id:」one」, content:」<h1>Part One</h1> <h2>你好:John,如今時間時 23:20 </h2>」,
css:」[..]「,
js:」[..]「,
…} );
</script>
這樣瀏覽器就會完成pagelet one的加載,其餘pagelet按此方法進行.它們之間不是順序執行的,而是並行執行,由於在服務端採用並行方式.上面即是整個bigpipe的執行過程了.
這裏以個人博客園我的首頁作例子,實現一個基於servlet的BigPipe簡單demo.由於是簡單的demo,沒有實現數據庫訪問和頁面的渲染操做,只是作一個簡單的模仿.
首先我將我的模塊主頁分爲header(頂部),sideBar(左邊),mainContent(文章列表),footer四個pagelet,寫了一個簡單的頁面框架,代碼以下:
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-cn"> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>JohnZheng - 博客園</title> <!-- JS 解析函數 --> <script type="application/javascript"> function replace(id,content) { var pagelet = document.getElementById(id); pagelet.innerHTML = content; } </script> </head> <body> <div style="margin:0px 0px;padding:0px 0px;"> <!-- header 頭部 --> <div id="header" style="height:150px;background-color:#00FFFF;"></div> <div style="clear:both"></div> <div id="main"> <!--sideBar 側邊欄容器 --> <div id="sideBar" style="width:200px;height:420px;float:left;background-color:#00ff00;"></div> <!--mainContent 主體內容容器--> <div id="mainContent" style="float:left;width:800px; height:420px;padding-left:10px;"></div> <div style="clear:both"></div> </div><!--end: main --> <div style="clear:both"></div> <!--footer --> <div id="footer" style="background-color:#C0C0C0;height:60px;text-align:center"></div> </div><!--end: home 自定義的最大容器 --> <!--</body>--> <!--</html>-->
這裏須要注意如下幾點:
顯示效果以下圖所示:
未各個pagelet編寫視圖模板,該視圖模板負責顯示對應的pagelet,規定了pagelet的顯示樣式.因此案例中編寫了header.ftl,sideBar.ftl, mainContent.ftl, footer.ftl. (這裏的文件名不須要和pagelet的div id對應).
頂部視圖模板 header.ftl:
<div> <h1><a href="http://www.cnblogs.com/jaylon/">John Zheng</a></h1> <h2>知止然後定,定然後能靜,靜然後能安,安然後能慮,慮然後能得.</h2> </div><!--end: blogTitle 博客的標題和副標題 --> <div id="navigator"> 博客園 首頁 新聞 新隨筆 聯繫 管理 訂閱 <div> <!--done--> 隨筆- 4 文章- 0 評論- 2 </div><!--end: blogStats --> </div><!--end: navigator 博客導航欄 -->
左邊視圖模板 sideBar.ftl
<div> <h3 class="catListTitle">公告</h3> 暱稱:JohnZheng</br> 園齡:6個月</br> 粉絲:4</br> 關注:1</br> </div>
文章列表視圖模板 mainContent.ftl
<div class="forFlow"> </br> <div class="day"> <div class="dayTitle"> <a href="http://www.cnblogs.com/jaylon/archive/2015/10/29.html">2015年10月29日</a> </div> <div> <a href="http://www.cnblogs.com/jaylon/p/4918914.html">Spring 入門知識點筆記整理</a> </div> <div ><div >摘要: spring入門學習的筆記整理,主要包括spring概念,容器和aop的入門知識點筆記的整理.<a href="http://www.cnblogs.com/jaylon/p/4918914.html" class="c_b_p_desc_readmore">閱讀全文</a></div></div> <div ></div> <div >posted @ 2015-10-29 19:59 JohnZheng 閱讀(395) 評論(2) <a href ="http://i.cnblogs.com/EditPosts.aspx?postid=4918914" rel="nofollow">編輯</a></div> </div> </br> <div > <div> <a href="http://www.cnblogs.com/jaylon/archive/2015/10/28.html">2015年10月28日</a> </div> <div > <a href="http://www.cnblogs.com/jaylon/p/4908075.html">spring遠程服務知識梳理</a> </div> <div> <div>摘要: 本文主要是對spring中的幾個遠程調度模型作一個知識梳理.spring所支持的RPC框架能夠分爲兩類,同步調用和異步調用.同步調用如:RMI,Hessian,Burlap,Http Invoker,JAX-WS. RMI採用java序列化,但很難穿過防火牆.Hessian,Burlap都是基於http協議,可以很好的穿過防火牆.但使用了私有的對象序列化機制,Hessian採用二進制傳送數據,而Burlap採用xml,因此Burlap能支持不少語言如python,java等.Http Invoker 是sping基於HTTP和java序列化協議的遠程調用框架,只能用於java程序的通行.Web service(JAX-WS)是鏈接異構系統或異構語言的首選協議,它使用SOAP形式通信,能夠用於任何語言,目前的許多開發工具對其的支持也很好. 同步通訊有必定的侷限性.因此出現了異步通訊的RPC框架,如lingo和基於sping JMS的RPC框架.<a href="http://www.cnblogs.com/jaylon/p/4908075.html">閱讀全文</a></div> </div> <div >posted @ 2015-10-28 14:31 JohnZheng 閱讀(424) 評論(0) <a href ="http://i.cnblogs.com/EditPosts.aspx?postid=4908075" rel="nofollow">編輯</a> </div> </div> </br> <div> <div > <a href="http://www.cnblogs.com/jaylon/archive/2015/10/24.html">2015年10月24日</a> </div> <div > <a href="http://www.cnblogs.com/jaylon/p/4905769.html">Spring Security 入門詳解</a> </div> <div ><div class="c_b_p_desc">摘要: spring security主要是對spring應用程序的安全控制,它包括web請求級別和方法調度級別的安全保護。本文是主要介紹spring security的基礎知識,對spring security所涉及的全部知識作一個梳理。<a href="http://www.cnblogs.com/jaylon/p/4905769.html" class="c_b_p_desc_readmore">閱讀全文</a></div></div> <div></div> <div>posted @ 2015-10-24 11:47 JohnZheng 閱讀(331) 評論(0) <a href ="http://i.cnblogs.com/EditPosts.aspx?postid=4905769" rel="nofollow">編輯</a></div> </div> </div><!--end: forFlow -->
尾部視圖模板 footer.ftl
</br>Copyright ©2015 JohnZheng
如今是構建一個Servlet負責將訪問我的博客首頁的請求用BigPipe方式來加載頁面.這裏我構建了一個BigPipeServlet實現bigPipe的整個流程.當用戶請求BigPipeServlet時
下面是BigPipeServlet.java的代碼:
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Content-type", "text/html;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); //這裏是獲得一個渲染的頁面框架 String frameView = Renderer.render("index.ftl"); //將頁面框架返回給前端 flush(writer,frameView);
//並行處理pagelet ExecutorService executor = Executors.newCachedThreadPool(); CompletionService<String> completionService = new ExecutorCompletionService<String>(executor); completionService.submit(new PageletWorker(1500,"header","pagelets/header.ftl")); //處理頭部 completionService.submit(new PageletWorker(2000,"sideBar","pagelets/sideBar.ftl"));//處理左邊信息 completionService.submit(new PageletWorker(4000,"mainContent","pagelets/mainContent.ftl"));//處理文章列表 completionService.submit(new PageletWorker(1000,"footer","pagelets/footer.ftl"));//處理尾部 //若是某個pagelet處理完成則返回給前端 try { for(int i = 0;i < 4; i++){ Future<String> future = completionService.take(); String result = future.get(); flush(writer,result); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //最後關閉頁腳 closeHtml(writer); } /** * 返回給前端 * @param writer * @param content */ private void flush(PrintWriter writer,String content){ writer.println(content); writer.flush(); } /** * 關閉頁面 * @param writer */ private void closeHtml(PrintWriter writer){ writer.println("</body>"); writer.println("</html>"); writer.flush(); writer.close(); }
這裏採用了ExecutorCompletionService來負責並行處理pagelet,ExecutorCompletionService是一個包含Executor 和阻塞隊列的類.提交的線程會交給Executor執行,執行的結果放到阻塞隊列中,因此只要從隊列中獲取數據就能夠了,簡化了多線程操做.
代碼中爲每一個pagelet設置了不一樣的處理時間,這樣在瀏覽器能夠看到不一樣頁面的在不一樣時間顯示.以上代碼中還引用了另外一個類,PageletWorker. 該類主要負責pagelet的業務邏輯處理,獲取所需數據及頁面的渲染.最終將渲染完成的頁面交給BigPipeServlet處理.PageletWorker.java代碼以下:
public class PageletWorker implements Callable<String> { //模擬完成業務邏輯的運行時間(pagelet所須要的數據,渲染頁面等的總時間) private int runtime; //pagelet視圖模板 private String pageletViewPath; private String pageletKey; /** * 建立一個pagelet 執行器.執行pagelet的業務邏輯和渲染頁面.這裏只是模擬. * @param runtime 進行業務處理,數據獲取等的運行時間
* @param pageletKey 對應html 中div id * @param pageletViewPath 模板的視圖路徑 */ public PageletWorker(int runtime,String pageletKey, String pageletViewPath) { this.runtime = runtime; this.pageletKey = pageletKey; this.pageletViewPath = pageletViewPath; } public String call() throws Exception { //模仿業務邏輯的處理和相關數據獲取時間 Thread.sleep(runtime); //模仿頁面渲染過程 String result = Renderer.render(pageletViewPath); result = buildJsonResult(result); return result; } /** * 將結果轉化爲json形式 * @param result * @return */ private String buildJsonResult(String result) { StringBuilder sb = new StringBuilder(); sb.append("<script type=\"application/javascript\">") .append("\nreplace(\"") .append(pageletKey) .append("\",\'") .append(result.replaceAll("\n","")).append("\');\n</script>"); return (String) sb.toString(); } }
該類主要邏輯以下:
Renderer.java 是模仿獲取渲染過程,這裏主要是讀取pagelet視圖內容.具體代碼以下:
public class Renderer { /** * 模範頁面的渲染,這裏主要是獲取對應的頁面信息. * @param viewPath * @return */ public static String render(String viewPath){ String absolutePath = Renderer.class.getClassLoader().getResource(viewPath).getPath(); File file = new File(absolutePath); StringBuilder contentBuilder = new StringBuilder(); BufferedReader br = null; try { br = new BufferedReader(new FileReader(file)); String str; while ((str = br.readLine()) != null) {//使用readLine方法,一次讀一行 contentBuilder.append(str + "\n"); } } catch (Exception e) { // TODO: handle exception }finally{ if(br != null) try { br.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return contentBuilder.toString(); } }
在web.xml中配置BigPipeServlet:
<servlet> <servlet-name>bigPipeServlet</servlet-name> <display-name>bigPipeServlet</display-name> <description></description> <servlet-class>org.opensjp.bigpipe.servlet.BigPipeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>bigPipeServlet</servlet-name> <url-pattern>/bigPipeServlet</url-pattern> </servlet-mapping>
在頁面輸入/bigPipeServlet就能夠看到效果了.先是顯示頁面框架內容,而後陸續的顯示各個pagelet的內容.
代碼下載地址:https://github.com/JohnZhengHub/BigPipe-ServletDemo
若是要實現一個具備良好性能的BigPipe,須要考慮的東西還挺多.能夠從一下幾個方面考慮,提升BigPipe框架的性能.
1.對搜索引擎的支持.
這是一個必須考慮的問題,現在是搜索引擎的時代,若是網頁對搜索引擎不友好,或者使搜索引擎很難識別內容,那麼會下降網頁在搜索引擎中的排名,直接 減小網站的訪問次數。在BigPipe 中,頁面的內容都是動態添加的,因此可能會使搜索引擎沒法識別。可是正如前面所說,在服務器端首先要根據user-agent 判斷客戶端是不是搜索引擎的爬蟲,若是是的話,則轉化爲原有的模式,而不是動態添加。這樣就解決了對搜索引擎的不友好。
2.將資源文件進行壓縮,提升傳輸速度.能夠考慮使用G-zip對css和js文件進行壓縮
3.對js文件進行精簡:對js 文件進行精簡,能夠從代碼中移除沒必要要的字符,註釋以及空行以減少js 文件的大小,從而改善加載的頁面的時間。精簡js 腳本的工具可使用JSMin,使用精簡後的腳本的大小會減小20%左右。這也是一個很大的提高。
4.將樣式表放在頂部
將html 內容所需的css 文件放在首部加載是很是重要的。若是放在頁面尾部,雖然會使頁面內容更快的加載(由於將加載css 文件的時間放在最後,從而使頁面內容先顯示出來),可是這樣的內容是沒有使用樣式表的,在css 文件加載進來後,瀏覽器會對其使用樣式表,即再次改變頁面的內容和樣式,稱之爲「無樣式內容的閃爍」,這對於用戶來講固然是不友好的。實現的時候將css 文件放在<head>標籤中便可。
5.將js放在底部
支持頁面動態內容的Js 腳本對於頁面的加載並無什麼做用,把它放在頂部加載只會使頁面更慢的加載,這點和前面的提到的css 文件恰好相反,因此能夠將它放在頁尾加載。