文章目錄
前言
在Web應用的整個構建過程中,隨着框架和庫的成型,我們往往迷糊地知道應用框架和庫,而不知道細節的實現。這篇文章簡要介紹在Web應用中設計的原理性知識及技術,對認識一個Web應用的具體實現有相對具體而全面的瞭解,可以幫助我們去學習和思考其他框架和庫的實現、
一、數據上傳
Node的HTTP請求在HTTP_Parser解析報文頭結束後,報文內容部分會通過data事件觸發,我們只需要以流的方式處理即可。
表單數據
默認的表單提交,請求頭中的Content-Type字段值爲application/x-www-form-urlencoded
其他格式
JSON類型請求頭中的Content-Type字段的值爲application/json;
XML類型請求頭中的Content-Type字段的值爲application/xml;
在Content-Type中可以附帶編碼信息。
Content-Type: application/json; charset=utf-8
附件上傳
通常的表單,其內容可以通過urlencoded的方式編碼內容形成報文體,再發送給服務器端,
特殊表單可以含有file類型的控件,以及需要指定表單屬性enctype爲multipart/form-data;
瀏覽器在遇到multipart/form-data表單提交時,構造的請求報文與普通表單完全不同。
Content-Type: multipart/form-data; boundary=AaB03x
Content-Length: 18231
它代表本次提交的內容是由多部分構成的,其中boundary=AaB03x指定的是每部分內容的分界符,AaB03x是隨機生成的一段字符串,報文體的內容將通過在它前面添加–進行分割,報文結束時在它前後都加上–表示結束。另外,Content-Length的值必須確保是報文體的長度。
數據上傳與安全
內存限制
在解析表單、JSON、XML部分,如果使用先保存用戶提交的所有數據,然後再進行解析處理,最後才傳遞給業務邏輯的策略,一旦數據量過大,將發生內存被佔光的情況,這種策略存在的潛在問題是它僅適合數據量小的提交數據。
攻擊者通過客戶端能夠十分容易地模擬僞造大量數據,如果攻擊者每次提交1 MB的內容,那麼只要併發請求數量一大,內存就會很快地被喫光。
要文件上傳會導致內存佔光的問題,可以。
- 限制上傳內容的大小,一旦超過限制,停止接收數據,並響應400狀態碼。
- 通過流式解析,將數據流導向到磁盤中,Node只保留文件路徑等小數據。
採用的上傳數據量進行限制可以通過Content-Length進行判斷:
數據是由包含Content-Length的請求報文判斷是否長度超過限制的,超過則直接響應413狀態碼;
對於沒有Content-Length的請求報文,略微簡略一點,在每個data事件中判定即可。一旦超過限制值,服務器停止接收新的數據片段。
如果是JSON文件或XML文件,極有可能無法完成解析。
對於上線的Web應用,添加一個上傳大小限制十分有利於保護服務器,在遭遇攻擊時,能鎮定從容應對。
CSRF(跨站請求僞造)
CSRF(Cross-Site Request Forgery) 中文意思爲跨站請求僞造。
以一個留言的例子來說明。假設某個網站有這樣一個留言程序,提交留言的接口如下所示:
http://domain_a.com/guestbook
用戶通過POST提交content字段就能成功留言。服務器端會自動從Session數據中判斷是誰提交的數據,補足username和updatedAt兩個字段後向數據庫中寫入數據,正常的情況下,誰提交的留言,就會在列表中顯示誰的信息。
如果某個攻擊者發現了這裏的接口存在CSRF漏洞,那麼他就可以在另一個網站(http://domain_b.com/attack)上構造了一個表單提交,如下所示:
<form id="test" method="POST" action="http://domain_a.com/guestbook">
<input type="hidden" name="content" value="vim是這個世界上最好的編輯器" />
</form>
<script type="text/javascript">
$(function () {
$("#test").submit();
});
</script>
這種情況下,攻擊者只要引誘某個domain_a的登錄用戶訪問這個domain_b的網站,就會自動提交一個留言。由於在提交到domain_a的過程中,瀏覽器會將domain_a的Cookie發送到服務器,儘管這個請求是來自domain_b的,但是服務器並不知情,用戶也不知情。
以上過程就是一個CSRF攻擊的過程。而這裏的示例僅僅是一個留言的漏洞,如果出現漏洞的是轉賬的接口,那麼其危害程度可想而知。
解決CSRF攻擊可以添加隨機值,在後端渲染的時候後臺產生一個隨機值,存放在表單中隱藏的input框,提交數據的時候一起提交到後臺進行校驗。
二、路由解析
文件路徑型
-
靜態文件
URL的路徑與網站目錄路徑一致,無需轉換,也非常直觀。
這種路由的處理也很簡單,將請求路徑對於的文件發送給客戶端即可。 -
動態文件
在MVC流行起來前,Web服務器根據URL路徑找到對應的文件,然後服務器根據文件後綴名尋找腳本解析器,並傳入HTTP請求上下文(動態生成HTML頁面)。
MVC
MVC模型的主要思想是將業務邏輯按職責分離,主要有:
- 控制器(Controller),一組行爲的集合
- 模型(Model),數據相關的操作和封裝
- 視圖(View),視圖的渲染
這是目前最爲經典的分層模式,它的工作模式如下:
- 路由解析,根據URL尋找對應的控制器和行爲;
- 行爲調用相關的模型,進行數據操作
- 數據操作結束後,調用視圖和相關數據進行頁面渲染,輸出到客戶端
如何根據URL做路由映射?可以有兩種實現方式:
- 手工關聯映射(適合小項目),由對應的路由文件來將URL映射到對應的控制器,手工映射對URL比較靈活,可以通過正則匹配、參數解析等手動指定對應路徑映射的控制器
缺點:如果URL變動,需要改動路由的映射。 - 通過自然關聯映射,路由按照一種約定的方式自然而然地實現了路由,無須維護路由映射,如
/controller/action/param1/param2/param3
以/user/setting/12/1987爲例,它會按約定去找controllers目錄下的user文件,將其require出來,調用這個文件模塊的setting()方法,而其餘的值作爲參數直接傳遞給這個方法。
這種自然映射的方式沒有指明參數的名稱,所以不能採用req.params的方式提取,直接通過路徑上的參數獲取。
RESTful
REST的全稱是Representational State Transfer,表現狀態轉化。符合REST規範的設計,我們稱爲RESTful設計。
它的設計哲學主要將服務器端提供的內容實體看作一個資源,並表現在URL上。
地址代表一個資源,對資源的操作主要體現在HTTP請求方法上,而不是URL。
原來對用戶增刪查改的URL可能如下,操作行爲主要體現在URL上,主要使用的請求方法是POST和GET:
POST /user/add?username=jacksontian
GET /user/remove?username=jacksontian
POST /user/update?username=jacksontian
GET /user/get?username=jacksontian
在RESTful的設計中是如下:
POST /user/jacksontian
DELETE /user/jacksontian
PUT /user/jacksontian
GET /user/jacksontian
它的設計引入了DELETE和PUT請求方法來參與資源的操作和更改資源的狀態。
在RESTful設計中,資源的具體格式由請求報頭中的Accept字段和服務器端的支持情況來決定。
如果客戶端同時接受JSON和XML格式的響應,它的Accept字段值爲:
Accept: application/json,application/xml
服務器端根據客戶端請求頭中Accept這個字段,再根據自身能響應的格式做出響應。
響應報文中通過Content-type字段告知客戶端是什麼格式。具體的格式,我們稱之爲具體的表現。
REST設計就是通過URL設計資源、請求方法定義資源的操作,通過Accept決定資源的表現形式。
RESTful與MVC的設計並不衝突,而是更好的改進。
相比MVC,RESTful只是將HTTP請求方法也加入了路由的過程,以及在URL路徑上體現得更資源化。
三、中間件
在最早的中間件的定義中,它是一種在操作系統上爲應用軟件提供服務的計算機軟件。它既不是操作系統的一部分,也不是應用軟件的一部分,它處於操作系統與應用軟件之間,讓應用軟件更好、更方便地使用底層服務。
如今中間件的含義借指了這種封裝底層細節,爲上層提供更方便服務的意義。
在Web應用中指的中間件,就是封裝了HTTP請求細節處理的中間件,簡化和隔離這些基礎設施與業務邏輯之間的細節,開發者可以脫離這部分細節而專注在業務上。
中間件的行爲類似Java中的過濾器(filter)的工作原理,在進入具體的業務處理之前,先讓過濾器處理。工作模型如下圖:
從HTTP請求到具體業務邏輯之間,其實有很多細節要處理。
Node的http模塊提供了應用層協議網絡的封裝,對具體業務並沒有支持(比如日誌、請求處理、鑑權等等),在業務邏輯下,必須有開發框架對業務提供支持。
通過中間件的形式搭建開發框架,這個開發框架用來組織各個中間件。對於Web應用的各種基礎功能,通過中間件處理相對簡單的邏輯最終匯成強大的基礎框架。
由於Node是異步的原因,需要提供一種機制,可以在當前中間件中處理完成後,通知下一個中間件執行。可以採用Connect的設計,通過尾觸發的方式實現。如:
var middleware = function (req, res, next) {
// TODO
next();
}
通過框架支持,可以將所有的基礎功能支持串聯起來,如下:
app.use('/user/:username', querystring, cookie, session, function (req, res) {
// TODO
});
use()中將中間件都存進stack數組中保存,等待匹配後觸發執行。
每個中間件執行完成後,按照約定調用傳入next()方法觸發下一個中間件執行(或直接響應),直到最後的業務邏輯。
爲每個路由都配置中間件看起來並不是很友好,如:
app.get('/user/:username', querystring, cookie, session, getUser);
app.put('/user/:username', querystring, cookie, session, updateUser);
//更多路由
中間件既然是對業務邏輯的封裝,那中間件和業務邏輯是等價的,我們可以將路由和中間件進行結合,這樣的設計看起來會更加簡潔友好 ,如上面的配置可以寫成:
app.use(querystring);
app.use(cookie);
app.use(session);
app.get('/user/:username', getUser);
app.put('/user/:username', authorize, updateUser)
中間件和路由的協作,可以將複雜的處理過程進行簡化,讓Web應用開發者可以只關注業務開發就可以完成整個複雜的開發工作。
異常處理
爲了保證Web應用的穩定性和健壯性,如果中間件出現錯誤,應該要進行處理捕獲。
由於中間件是使用尾調用的方式實現 ,我們可以爲next()方法添加err參數,並捕獲中間件直接拋出的同步異常。
由於異步方法的異常不能直接捕獲,所以對於中間件異步產生的異常需要自己傳遞出來,如下:
var session = function (req, res, next) {
var id = req.cookies.sessionid;
store.get(id, function (err, session) {
if (err) {
// 將異常通過next()傳遞
return next(err);
}
req.session = session;
next();
});
};
Next()方法接到異常對象後,會將其交給handle500()進行處理。
爲了延續中間件的思想,異常處理的中間件也是可以採用數組式進行處理。
因爲要同時傳遞異常,所以用於處理異常的中間件的設計與普通中間件略有區別。它的參數有4個:
var middleware = function (err, req, res, next) {
// TODO
next();
};
通過use()可以將所有異常處理的中間件註冊起來,如下:
app.use(function (err, req, res, next) {
// TODO
});
爲了區分普通中間件和異常處理中間件,handle500()方法將會對中間件按參數進行進行選取,然後遞歸執行。
var handle500 = function (err, req, res, stack) {
// 選取異常處理中間件
stack = stack.filter(function (middleware) {
return middleware.length === 4;
});
var next = function () {
// 從stack數組中取出中間件並執行
var middleware = stack.shift();
if (middleware) {
// 傳遞異常對象
middleware(err, req, res, next);
}
};
// 啓動執行
next();
};
中間件與性能
在中間件中我們的業務邏輯 往往是最後才執行的,爲了可以讓業務邏輯儘早提前執行響應給終端用戶,所以中間件的編寫和使用需要進行考究,主要從兩個方面進行提升:
一. 編寫高效的中間件
高效的中間件就是提升單個處理單元的處理速度,以儘早調用next() 執行後續邏輯。一旦中間件被匹配,那個每個請求都會使該中間件執行一次,即使只耗費1豪秒的執行時間,也會使QPS顯著下降。
- 使用高效的方法,必要時通過jsperf.com測試基準性能
- 緩存需要重複計算的結果(需要控制緩存用量,防止內存達到限制)
- 避免不必要的計算,比如HTTP報文體的解析 ,對GET方法完全不需要。
二. 合理利用路由,避免不必要的中間件執行
合理使用路由,不必要的路由不參與請求處理過程,比如
我們有一個靜態文件的中間件,它會對請求進行判斷,如果磁盤上存在對應文件就會響應對應的靜態文件,否則就交由下游中間件處理,
如果使用如下方式註冊路由:
app.use(staticFile);
那麼意味着對 / 路徑下的所有URL請求都會進行判斷,對於沒不能成功匹配的更是造成了性能的浪費,使QPS直線下降。
所以需要提高匹配成功率,不能使用默認的 / 路徑來匹配,這個不匹配造成的性能 浪費代價太高,可以添加一個更好的路由路徑,如下:
app.use('/public',staticFile);
這樣 只有/public路徑會匹配上,其餘路徑不會涉及該中間件。
所以,對於中間件來說,它是對及基礎功能收斂成規整的組織形式。對於單箇中間件來說,它應該足夠簡單,職責單一,具備更好的可測試性。
中間件機制使得Web應用具備良好的可擴展性和可組合性。中間件是Connect的經典模式。
四、內容響應和頁面渲染
服務器端的響應有可能是一個HTML網頁、CSS、JS文件,或者其他多媒體文件。
對於過去流行的ASP、PHP、JSP等動態網頁技術,頁面渲染是一種內置的功能。但對於Node來說並沒有這樣的內置功能。
內容響應
http模塊封裝了對請求報文和響應報文的操作。服務器端響應的報文,最終都要被終端處理。這個終端可能是命令行終端,也可能是代碼終端,也可能是瀏覽器。
服務器端的響應從一定程度上決定或指示了客戶端該如何處理響應的內容。
內容響應的過程,響應報頭中的Content-*字段十分重要。如下的響應報文中,服務端告訴客戶端內容是以gzip編碼的,其內容長度爲21170個字節,內容類型爲JavaScript,字符集爲UTF-8:
Content-Encoding: gzip
Content-Length: 21170
Content-Type: text/javascript; charset=utf-8
客戶端在接受到這個報文後,會通過gzip來解碼報文體中的內容,用長度校驗報文體內容是否正確,然後再以字符集UTF-8將解碼後的腳本插入到文檔節點中。
MIME
瀏覽器通過不同的Content-Type的值來決定採用不同的渲染方式,這個值簡稱爲MIME值。
MIME的全稱是Multipurpose Internet Mail Extensions(多功能互聯網郵件擴展),從名字可以看出最早用於電子郵件,後來也應用到瀏覽器中。
不同的文件類型具有不同的MIME值,如:
JSON文件的值爲application/json、
XML文件的值爲application/xml、
PDF文件的值爲application/pdf
除了MIME值外,Content-Type的值中還可以包含一些參數,如字符集:
Content-Type: text/javascript; charset=utf-8
社區有專用的mime模塊可以用來判斷文件類型。
附件下載
在一些場景下,無論響應的內容是什麼樣的MIME值,需求中並不要求客戶端去打開它,只需要彈出並下載它即可。
Content-Disposition字段正是爲了滿足這種需求,該字段影響的行爲是客戶端會根據它的值判斷是應該將報文數據當做即時瀏覽的內容,還是可以下載的附件。
Content-Disposition: inline // 內容只需即時查看
Content-Disposition: attachment //數據可以存爲附件
另外,Content-Disposition字段還能通過參數指定保存時應該使用的文件名。示例如下:
Content-Disposition: attachment; filename="filename.ext"
如果要設計一個響應附件下載的API(res.sendfile),方法大致是如下這樣的:
res.sendfile = function (filepath) {
fs.stat(filepath, function(err, stat) {
var stream = fs.createReadStream(filepath);
// 設置內容
res.setHeader('Content-Type', mime.lookup(filepath));
// 設置長度
res.setHeader('Content-Length', stat.size);
// 設置爲附件
res.setHeader('Content-Disposition' 'attachment; filename="' + path.basename(filepath) + '"');
res.writeHead(200);
stream.pipe(res);
});
};
響應JSON
爲了快捷地響應JSON數據,我們也可以如下這樣進行封裝:
res.json = function (json) {
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify(json));
};
響應跳轉
當我們的URL因爲某些問題(譬如權限限制)不能處理當前請求,需要將用戶跳轉到別的URL時,我們也可以封裝出一個快捷的方法實現跳轉,如下所示:
res.redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end('Redirect to ' + url);
};
視圖渲染
Web的響應有很多種,包括附件和跳轉等,而對於普通HTML內容響應,最終呈現在界面上的內容,稱爲視圖渲染。
在動態頁面技術中,最終的視圖是由模板和數據共同生成出來的。
模板是帶有特殊標籤的HTML片段,通過與數據的渲染,將數據填充到這些特殊標籤中,最後生成普通的帶數據的HTML片段。
通常我們將渲染方法設計爲render(),參數就是模板路徑和數據,如下所示:
res.render = function (view, data) {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
// 實際渲染
var html = render(view, data);
res.end(html);
};
在Node中,數據自然是以JSON爲首選,但是模板卻有太多選擇可以使用了。上面代碼中的render()我們可以將其看成是一個約定接口,接受相同參數,最後返回HTML片段。這樣的方法我們都視作實現了這個接口。
模板
最早的服務器端動態頁面開發,是在CGI程序或servlet中輸出HTML片段,通過網絡流輸出到客戶端,客戶端將其渲染到用戶界面上。
這種邏輯代碼與HTML輸出的代碼混雜在一起的開發方式,導致一個小小的UI改動都要大動干戈,甚至需要重新編譯。
爲了改良這種情況,使HTML與邏輯代碼分離開來,催生出一些服務器端動態網頁技術,如ASP、PHP、JSP。它們將動態語言部分通過特殊的標籤(ASP和JSP以<% %>作爲標誌,PHP則以<? ?>作爲標誌)包含起來,通過HTML和模板標籤混排,將開發者從輸出HTML的工作中解脫出來。它們其實就是最早的模板技術。
這樣的方法雖然一定程度上減輕了開發維護的難度,但是頁面裏還是充斥着大量的邏輯代碼。這催生了MVC在動態網頁技術中的發展,MVC將邏輯、顯示、數據分離開來的方式,大大提高了項目的可維護性。其中模板技術就在這樣的發展中逐漸成熟起來的。
模板技術的實質就是將模板文件和數據通過模板引擎生成最終的HTML代碼。
形成模板技術的有如下4個要素。
- 模板語言。
- 包含模板語言的模板文件。
- 擁有動態數據的數據對象。
- 模板引擎。
由於各種語言採用的模板語言不同,包含各種特殊標記,導致移植性較差。
早期的企業一旦選定編程語言就不會輕易地轉換環境,所以較少有開發者去開發新的模板語言和模板引擎來適應不同的編程語言。如今異構系統越來越多,模板能夠應用到多門編程語言中的這種需求也開始呈現出來。
破局者是Mustache,它宣稱自己是弱邏輯的模板(logic-less templates),定義了以{{}}爲標誌的一套模板語言。
隨着Node在社區的發展,與模板引擎相關模塊的實現多不勝數,並且由於Node與前端都採用相同的執行語言JavaScript,所以一套模板語言也無須爲它編寫兩套不同的模板引擎就能輕鬆地跨前後端共用。
模板和數據與最終結果相比,這裏有一個靜態、動態的劃分過程,相同的模板和不同的數據可以得到不同的結果,不同的模板與相同的數據也能得到不同的結果。
模板技術使得網頁中的動態內容和靜態內容變得不互相依賴,數據開發者與模板開發者只要約定好數據結構,兩者就不用互相影響了。
模板技術乾的實際上是拼接字符串這樣很底層的活,只是各種模板有着各自的優缺點和技巧。我們要的就是模板加數據,通過模板引擎的執行就能得到最終的HTML字符串這樣結果。
假設我們的模板是如下這樣,<% =%>就是我們制定的模板標籤
Hello <%= username%>
如果我們的數據是{username: “JacksonTian”},那麼我們期望的結果就是Hello JacksonTian。
具體實現的過程是模板分爲Hello和<%= username%>兩個部分,前者爲普通字符串,後者是表達式。表達式需要繼續處理,與數據關聯後成爲一個變量值,最終將字符串與變量值連成最終的字符串。
渲染過程爲:
模板引擎
模板引擎的流程大致爲:
- 語法分解。提取出普通字符串和表達式,這個過程通常用正則表達式匹配出來,<%=%>的正則表達式爲/<%=([\s\S]+?)%>/g。
- 處理表達式。將標籤表達式轉換成普通的語言表達式。
- 生成待執行的語句。
- 與數據一起執行,生成最終字符串。
模板編譯
與數據一起執行,生成最終字符串,這個過程稱爲模板編譯,生成的中間函數只與模板字符串相關,與具體的數據無關。
如果每次都生成這個中間函數,就會浪費CPU。爲了提升模板渲染的性能速度,我們通常會採用模板預編譯的方式。
通過預編譯緩存模板編譯後的結果,實際應用中就可以實現一次編譯,多次執行,而原始的方式每次執行過程中都要進行一次編譯和執行。
with的應用
模板安全
模板編譯會把普通字符串直接輸出,變量標籤替換成變量如obej[code], 如果變量是惡意腳本字符串就會構成XSS漏洞,在執行腳本的時候產生安全問題。
爲了提高安全性,所以大多數模板會提供轉義功能,將能形成HTML標籤的字符串轉換成安全的字符,這些字符主要有&、<、>、"、 ’ 等 ,把這些字符進行轉義。
不確定要輸出HTML標籤的字符最好都轉義,特別是用戶的輸入內容。
可以使用不同的標籤區別表示轉義和非轉義,比如:<%=%>和<%-%>
模板邏輯
儘管模板技術將業務邏輯與視圖部分分離開來,但是在視圖上允許使用一些邏輯來控制頁面的最終渲染,可以讓模板變得更強大。
集成文件系統
對於客戶端的請求我們如果每次都需要對對應的文件進行編譯然後再渲染,這樣會造成:
- 每次請求都需要反覆讀磁盤上的模板文件
- 從每次請求都需要編譯
- 調用繁瑣
所以我們需要更簡潔、性能更好的render()函數。可以使用緩存將請求編譯過的文件緩存起來,這樣就不會再反覆地讀取和編譯文件了,調用起來也很輕鬆。
這樣與文件系統集成並且引入緩存,可以很好地解決性能問題,接口也可以得到簡化,而且模板文件內容不大,也不屬於動態改動,所以使用進程內存緩存編譯結果,並不會引起太大的垃圾回收問題。
子模版
有時候模板文件太大,太過複雜,會增加維護難度,而且有些模板是可以重用的,所以產生了子模版。
子模版可以嵌套在別的模板中,維護多個子模版比維護完整而複雜的大模板容易一些,可以將複雜的問題降解爲多個小而簡單的問題。
佈局視圖
佈局視圖又稱母版頁,它與子模版的原理相同,當頁面內容有所差別,但是頁面佈局相同,就可以將佈局模板重用起來,子模版嵌入佈局模板中使用。
模板性能
模板引擎的優化主要有以下幾種:
- 緩存模板文件
- 緩存模板文件編譯後的函數
- 優化模板中的執行表達式
在完成前面兩步後,渲染的性能就與生成的函數直接相關,這個函數與模板字符串的複雜度*有直接關係。
如果在模板中編寫了執行表達式,執行表達式的性能將直接影響模板的性能。優化執行表達式就是對模板性能的優化。
模板引擎的實現方式也會影響到性能。比如用new Function()生成函數,還可以用eval();
對於字符串的處理,可以使用字符串直接相加,也可以採用數組存儲的 方式,最後將所有字符串直接相連;
對於變量的查找,有的使用with形成作用域的方式實現查找,也可以使用變量名進行查找,這樣相比with不用切換上下文。
這些都是影響模板的速度的因素。
Bigpipe
Bagpipe的翻譯爲風笛,是用於調用限流。它是產生於Facebook公司的前端加載技術,它的提出主要是爲了解決重數據頁面的加載速度問題。
舉個例子,當我們要獲取用戶主頁的時候,需要獲取用戶的數據和用戶歷史文章列表,那可以同時異步並行請求多個接口的數據,拿到相關數據後再填充到模板上返回客戶端。但是在頁面組裝好數據返回到客戶端之前,用戶看到的都是空白的頁面,這樣是十分不友好的體驗。
Bigpipe的解決思路是將頁面分割成多個部分(pagelet),先向用戶輸出沒有數據的佈局(框架),將每個部分逐步輸出到前端,在最終渲染填充框架,完成整個頁面的渲染。這個過程需要前端JavaScript的參與,它負責將後續輸出的數據渲染到頁面上。
Bigpipe是一個需要前後端配合實現的優化技術,這個技術有幾個重要點:
-
頁面佈局框架(無數據的)
在佈局文件中需要引入必要的前端腳本,以及需要到的庫,其次要引入我們重要的前端腳本,命名爲Bigpipe.js,文件內容大致爲:var Bigpipe=function(){ this.callbacks={}; } Bigpipe.prototype.ready=function(key,callback){ if(!this.callbacks[key]){ this.callbacks[key]=[]; } this.callbacks[key].push(callback); } Bigpipe.prototype.set=function(key,data){ var callbacks=this.callbacks[key]||[]; for(var i=0;i<callbacks.length;i++){ callbacks[i].call(this, data); } }
-
後端持續性的數據輸出
在模板輸出後,整個頁面的渲染並沒有結束,但用戶已經可以看到整個頁面大體的樣子,然後再繼續數據輸出。數據異用調用後先返回的數據可以先推送到頁面上,不過和普通數據輸出不同,這裏的數據輸出之後需要被前端腳本處理,所以需要對它進行封裝處理。把數據用js腳本的形式返回頁面執行,然後bigpipe對象拿到對應的數據再渲染到頁面上。db.getData('sql1', function (err, data) { data = err ? {} : data; res.write('<script>bigpipe.set("articles", ' + JSON.stringify(data) + ');</script>'; });
-
前端渲染
Bigpipe.ready()和Bigpipe.set()是整個前端的渲染機制,前者用一個key註冊時間,後者則觸發一個事件,以此完成頁面的渲染機制。
Bigpipe這樣一個逐步渲染頁面的過程,其實通過Ajax也能完成,但是Ajax背後是HTTP調用,要耗費更多的網絡連接,而Bigpipe獲取數據則與當前頁面共用相同的網絡連接,開銷十分小。
不過完成Bigpipe要設計的細節比較多,比MVC中的直接渲染要複雜許多,所以建議在重要且數據請求時間比較長的頁面中使用。