教你編寫 Node.js 中間件,實(shí)現(xiàn)服務(wù)端緩存(附demo源碼)

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!

PS:
作者Github倉庫知乎問答鏈接
歡迎各種形式交流藐窄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市酬土,隨后出現(xiàn)的幾起案子荆忍,更是在濱河造成了極大的恐慌,老刑警劉巖撤缴,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刹枉,死亡現(xiàn)場離奇詭異,居然都是意外死亡屈呕,警方通過查閱死者的電腦和手機(jī)微宝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虎眨,“玉大人蟋软,你說我怎么就攤上這事∷宰” “怎么了岳守?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長碌冶。 經(jīng)常有香客問我湿痢,道長,這世上最難降的妖魔是什么扑庞? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任譬重,我火速辦了婚禮拒逮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘臀规。我一直安慰自己消恍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布以现。 她就那樣靜靜地躺著狠怨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邑遏。 梳的紋絲不亂的頭發(fā)上佣赖,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天,我揣著相機(jī)與錄音记盒,去河邊找鬼憎蛤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纪吮,可吹牛的內(nèi)容都是我干的俩檬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼碾盟,長吁一口氣:“原來是場噩夢啊……” “哼棚辽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冰肴,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤屈藐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后熙尉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體联逻,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年检痰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了包归。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,664評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铅歼,死狀恐怖公壤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谭贪,我是刑警寧澤境钟,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布锦担,位于F島的核電站俭识,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏洞渔。R本人自食惡果不足惜套媚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一缚态、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧堤瘤,春花似錦玫芦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至慎皱,卻和暖如春老虫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茫多。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工祈匙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人天揖。 一個(gè)月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓夺欲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親今膊。 傳聞我的和親對象是個(gè)殘疾皇子些阅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)斑唬,斷路器扑眉,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,790評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件赖钞、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,066評論 4 62
  • 喜歡 一個(gè)人行走 一種沉靜腰素,一種心境,一種心靈的徜徉 在黃昏雪营,在清晨弓千,在靈魂的最深處 在月下,在雨中献起,在淡靜的大自...
    四夕清荷閱讀 215評論 0 1
  • 青春成全夢想 與其說 潮水剝奪了沙灘的自由洋访, 不如說 沙灘成全了潮水的澎湃。 與其說 夢想犧牲了青春谴餐, 不如說 青...
    新觀點(diǎn)讀書閱讀 221評論 4 1