Express 作為 Node.js 的框架,如今發(fā)展可謂如日中天瓷叫。我很喜歡其靈活太雨、易擴(kuò)展的設(shè)計(jì)理念抚太。尤其是該框架的中間件架構(gòu)設(shè)計(jì):使得在應(yīng)用中加入新特性更加標(biāo)準(zhǔn)化坪它、成本最小化。這篇文章蛾派,我會(huì)嘗試編寫一個(gè)非常簡單俄认、小巧的中間件个少,完成服務(wù)端緩存功能,進(jìn)而優(yōu)化性能眯杏。
關(guān)于中間件
說到中間件夜焦,Express 官網(wǎng)對它的闡述是這樣的:
“Express 是一個(gè)自身功能極簡,完全是路由和中間件構(gòu)成一個(gè)web開發(fā)框架:從本質(zhì)上來說岂贩,一個(gè) Express 應(yīng)用就是在調(diào)用各種中間件茫经。”
也許你使用過各種各樣的中間件進(jìn)行開發(fā)萎津,但是可能并不理解中間件原理卸伞,也沒有深入過 Express 源碼,探究其實(shí)現(xiàn)锉屈。這里并不打算長篇大論幫您分析荤傲,但是使用層面上大致可以參考下圖:
建議有興趣、想深入的讀者自己分析颈渊,有任何問題歡迎與我討論遂黍。即便您不打算深入,也不會(huì)影響對下文中間件編寫的理解俊嗽。
關(guān)于服務(wù)端緩存
緩存已經(jīng)被廣泛應(yīng)用雾家,來提高頁面性能。一說到緩存乌询,可能讀者腦海里馬上冒出來:“客戶端緩存榜贴,CDN 緩存豌研,服務(wù)器端緩存......”妹田。另一維度上,也會(huì)想到:“200(from cache)鹃共,expire鬼佣,eTag......”等概念。
當(dāng)然作為前端開發(fā)者霜浴,我們一定要明白這些緩存概念晶衷,這些緩存理念是相對于某個(gè)具體用戶訪問來說的,性能優(yōu)化體現(xiàn)在單個(gè)用戶上阴孟。比如說晌纫,我第一次打開頁面 A,耗時(shí)超長永丝,下一次打開頁面由于緩存的作用锹漱,時(shí)間縮短了。
但是在服務(wù)器端慕嚷,還存在另外一個(gè)維度哥牍,思考一下這樣的場景:
我們有一個(gè)靜態(tài)頁面 B毕泌,這個(gè)頁面服務(wù)端需要從數(shù)據(jù)庫獲取部分?jǐn)?shù)據(jù) b1,根據(jù) b1 又要計(jì)算得到部分?jǐn)?shù)據(jù) b2嗅辣,還得做各種高復(fù)雜度操作撼泛,最終才能“東拼西湊”出需要返回的完整頁面 B,整個(gè)過程耗時(shí)2s澡谭。
那么面臨的災(zāi)難就是愿题,user1 打開頁面耗時(shí)2s,user2同樣打開頁面耗時(shí)2s......而這些頁面都是靜態(tài)頁面 B译暂,內(nèi)容是完全一樣的抠忘。為了解決這個(gè)災(zāi)難,這時(shí)候我們也需要緩存外永,這種緩存就叫先做服務(wù)端緩存(server-side cache)崎脉。
總結(jié)一下,服務(wù)端緩存的目的其實(shí)就是對于同一個(gè)頁面請求伯顶,而返回(緩存的)同樣的頁面內(nèi)容囚灼。這個(gè)過程完全獨(dú)立于不同的用戶。
上面的話有些拗口祭衩,可以參考英文表達(dá)更清晰:
The goal of server side cache is responding to the same content for the same request independently of the client’s request.
因此灶体,下面展示的 demo 在第一次請求到達(dá)時(shí),服務(wù)端耗費(fèi)5秒來返回 HTML掐暮;而接下來再次請求該頁面蝎抽,將會(huì)命中緩存,不過是哪個(gè)用戶訪問路克,只需要幾毫秒便可得到完整頁面樟结。
Show me the code & Demo
其實(shí)上文提到的緩存概念非常簡單,稍微有些后端經(jīng)驗(yàn)的同學(xué)都能很好理解精算。但是這篇文章除去科普基本概念外瓢宦,更重要的就是介紹 Express 中間件思想,并自己來實(shí)現(xiàn)一個(gè)服務(wù)端緩存中間件驮履。
讓我們開工吧!
最終 Demo 代碼廉嚼,歡迎訪問它的Github地址玫镐。
我將會(huì)使用 npm 上 memory-cache 這個(gè)包,以方便進(jìn)行緩存的讀寫怠噪。最終的中間件代碼很簡單:
'use strict'
var mcache = require('memory-cache');
var cache = (duration) => {
return (req, res, next) => {
let key = '__express__' + req.originalUrl || req.url
let cachedBody = mcache.get(key)
if (cachedBody) {
res.send(cachedBody)
return
} else {
res.sendResponse = res.send
res.send = (body) => {
mcache.put(key, body, duration * 1000);
res.sendResponse(body)
}
next()
}
}
}
為了簡單恐似,我使用了請求 URL 作為 cache 的 key:
- 當(dāng)它(cache key)及其對應(yīng)的 value 值存在時(shí),便直接返回其 value 值舰绘;
- 當(dāng)它(cache key)及其對應(yīng)的 value 值不存在時(shí)蹂喻,我們將對 Express send 方法做一層攔截:在最終返回前葱椭,存入這對 key-value。
緩存的有效時(shí)間是10秒口四。
最終在判斷之外孵运,我們的中間件把控制權(quán)交給下一個(gè)中間件。
最終使用和測試如下代碼:
app.get('/', cache(10), (req, res) => {
setTimeout(() => {
res.render('index', { title: 'Hey', message: 'Hello there', date: new Date()})
}, 5000) //setTimeout was used to simulate a slow processing request
})
我使用了 setTimeout 來模擬一個(gè)超長(5s)的操作蔓彩。
打開瀏覽器控制面板治笨,發(fā)現(xiàn)在10秒緩存到期以內(nèi):
至于為什么 cache 中間件要那樣子寫、next() 為什么是中間件把控制權(quán)傳遞赤嚼,我并不打算展開去講旷赖。有興趣的讀者可以看一下 Express 源碼。
還有幾個(gè)小問題
仔細(xì)看我們的頁面更卒,再去體會(huì)一下實(shí)現(xiàn)代碼等孵。也許細(xì)心的讀者能發(fā)現(xiàn)一個(gè)問題:剛才的實(shí)現(xiàn)我們緩存了整個(gè)頁面,并將 date: new Date() 傳入了 jade 模版 index.jade 里蹂空。那么俯萌,在命中緩存的條件下,10秒內(nèi)上枕,頁面無法動(dòng)態(tài)刷新來同步咐熙,直到10秒緩存到期。
同時(shí)辨萍,我們什么時(shí)候可以使用上述中間件棋恼,進(jìn)行服務(wù)端緩存呢?當(dāng)然是靜態(tài)內(nèi)容才可以使用锈玉。同時(shí)爪飘,PUT, DELETE 和 POST 操作都不應(yīng)該進(jìn)行類似的緩存處理。
同樣嘲玫,我們使用了 npm 模塊:memory-cache悦施,它存在優(yōu)缺點(diǎn)如下:
- 讀寫迅速而簡單并扇,不需要其他依賴去团;
- 當(dāng)服務(wù)器或者這個(gè)進(jìn)程掛掉的時(shí)候,緩存中的內(nèi)容將會(huì)全部丟失穷蛹。
- memcache 是將緩存內(nèi)容存放在了自己進(jìn)程的內(nèi)存中土陪,所以這部分內(nèi)容是無法在多個(gè) Node.js 進(jìn)程之間共享的。
如果這些弊端 really matter肴熏,在實(shí)際開發(fā)中我們可以選擇分布式的 cache 服務(wù)鬼雀,比如 Redis。同樣你可以在 npm 上找到:express-redis-cache 模塊使用蛙吏。
總結(jié)
在真實(shí)的開發(fā)場景中源哩,服務(wù)端緩存已經(jīng)成為 common sense鞋吉,但是在 Node.js 的世界里,體會(huì)其中間件思想励烦,自己手動(dòng)編寫服務(wù)谓着,同樣樂趣無窮。
與實(shí)踐相結(jié)合坛掠,我認(rèn)為真正緩存整個(gè)頁面(如同 demo 那樣)并不是一個(gè)推薦的做法(當(dāng)時(shí)實(shí)際場景實(shí)際分析)赊锚,同樣使用請求 url 作為緩存的 key 也有待考慮。比如屉栓,頁面中的一些靜態(tài)內(nèi)容可能會(huì)在其他頁面中重復(fù)使用到舷蒲,復(fù)用就成了問題。
真實(shí)場景下友多,一切設(shè)計(jì)和邏輯都要為自己業(yè)務(wù)情況所負(fù)責(zé)牲平。脫離需求談實(shí)現(xiàn),都是耍流氓域滥。這個(gè) demo 簡易輕巧欠拾,有需要的讀者可以訪問它的Github地址,歡迎玩出各種花樣骗绕。
Happy Coding!