Johnny Wang BlogJohnny Wang Blog
Articles
Project
Live2d
johnnywang/book
Articles
Project
Live2d
johnnywang/book
  • About Me

    • 關於 Johnny
    • 文章清單
  • Javascript

    • View Transitions - 認識並使用原生 document 轉場效果
    • 用 Web Container 打造自己的線上 NodeJS 開發環境
    • 來試用看看原生 Web Popover API
    • 在電腦裡搞一個 RWKV AI 小助手
    • 遊戲 App 素材解包學習紀錄
    • 動手自己做一個 ChatGPT UI 工具吧
    • Create a React Server Components Project without NextJS - 製作一個不依賴 NextJS 的 React Server Components 專案
    • 如何在 Vuepress 裡快樂寫 React
    • 從 Mock Service Worker 源碼中學習
    • 擺脫 Node modules 地獄,擁抱 Yarn Plug'n'Play(PnP)
    • 快速上手 NextJS v13 - 基礎觀念 AppRouter 篇
    • 快速上手 NextJS v13 - Data Fetching, Caching, Revalidating 篇
    • 用 Socket.io 搭配 Matterjs 製作一款 Real-Time Canvas 聊天室(文長慎入)
    • 如何只用一支 CDN 及 4行設定,讓瀏覽器讀懂 Typescript, React, Vue
    • 用 tsup 快速建立 Typescript 開發環境
    • 開發 Email EDM 你可以更輕鬆
    • 關於我的 Side project - Maju Web Editor
    • Web URL 中神奇的 createObjectURL method
    • Awesome Vite 一個由社群維護的 template 集大成
    • Vue、React 使用心得分享(文長慎入)
    • 一起動手用 Socket.io 和 Peerjs 打造 WebRTC 即時視訊
    • Temporal Typescript SDK 學習筆記
    • React Web3 Storage
    • React useState 取得最新值
    • Babel 7 Decorator 的神奇小問題
    • 用 Koa + Vite + Pinia 打造基礎 SSR 環境
    • Live2d 官方範例改寫
    • 如何把 Video 畫在 Canvas 上
    • 史上最簡單的 Webpack 5 教學
    • 在瀏覽器中直接 import Vue SFC 開發起來
    • 手寫一個可中斷的 delay promise
    • NodeJS 輕量開發框架 Expressjs 與 Koa2 的區別
      • 市場定位
      • 建立 Server
      • Request/Response
      • 設置 logger
      • Middleware 中間件
      • 結論
      • 參考
    • 用 2D 物理引擎 Matterjs 製作經典馬力歐 1-1
    • 原生 Javascript 的類型標註工具 JSDoc
    • Pinia - Vuex 的後繼者
    • 用 Nodejs 寫個 FTP command line 工具
    • Vue3 Server Render 手把手帶你搭建
    • 你真的懂 Event Loop 嗎
    • Babel7 基本介紹與使用
    • Vue/Vitejs 部分源碼解析
    • Vuejs 依賴追蹤 2020 版
    • Vuejs 依賴追蹤 2019 版
    • Js literal 模板編譯
    • 用 JavaScript 鎖定用戶調整畫面比例
    • Date.now() 與 new Date()
    • Promise 相關知識
    • Web Component 學習筆記
    • TypeScript 基礎篇
    • TypeScript 進階篇
    • MVC, MVVM, MVI 軟體設計架構
    • MVVM 簡單模擬框架實作
  • CSS & Sass

    • 2024 CSS 年度報告筆記
    • 2021 CSS 年度報告筆記
    • 如何不用 setTimeout 幫 display: none 的 DOM 加動畫
    • 純 CSS 實作星球環繞動畫效果
    • 差點錯過的 Tailwindcss 入門學習筆記
    • Tailwindcss 進階學習筆記
    • Sass, SCSS Built-In-Modules 內建模組與函數
    • SCSS Parent Selector
    • CSS 畫面鎖橫屏時,滾動的問題!!
    • Safari 使用 animation 時動態產生 rem 的坑
    • CSS: mix-blend-mode 屬性混合圖層動畫
  • Memo

    • Javascript

      • Yarn Plug'n'Play (PnP) 的 VSCode 設定方式
      • 如何用 Web API 讀取剪貼簿內容
      • CSP (Content Security Policy) 是什麼?
      • Astro 入門學習筆記
      • 如何解決 Astro 套用主題切換時,畫面抖動瞬閃問題
      • Rspack - 以 Rust 打造的快速構建工具
      • Typescript v5 Decorator 學習筆記
      • Sequelize 筆記
      • ES2022 學習筆記
      • Crypto 密碼加密方法
      • 實作 Test 的注意事項
      • Jest 測試工具 - 基礎篇
      • 正則表達式
      • Rxjs 學習筆記
      • Gulp 學習筆記
      • Clean Code Javascript 學習
      • JS 實戰開發特殊小技巧
    • React

      • React forwardRef 使用筆記
      • 如何在 Nextjs 中使用 middleware set cookie
      • React Styled-Components 基礎篇
      • React Styled-Components 進階篇
      • React Unit Test
      • React Testing Library
      • React Emotion Basic
      • React Styled-JSX
      • React 18 - Concurrent Features Memo
      • useEffect 的高階封裝範例
      • React useContext & useReducer 搭配
      • React Router Config 筆記
    • Vue

      • Cypress Vue
      • Vue 單元測試學習筆記
      • 學習 Vuetify 的一些筆記
      • Vuex 學習筆記
    • React Native

      • React Native - basic
      • Expo Basic
      • Expo Router
      • Expo Build
      • Issues
    • GraphQL

      • GraphQL 學習筆記 - 基礎篇
      • GraphQL 學習筆記 - 進階篇
      • Apollo Client 學習使用筆記
      • GraphQL Memo
    • Parse

      • Parse Javascript 文檔閱讀筆記
      • Parse User Object 章節
      • Parse Session Object 章節
      • Parse Schema Object 章節
      • Parse Cloud Code 章節
    • CSS

      • Sass, SCSS 基礎筆記
    • Docker

      • Docker 基礎技術
      • Dockerfile 技術篇
      • Docker-compose 技術篇 - 3.7
      • Kubernetes 學習筆記
      • ArgoCD 學習筆記
      • Podman 學習筆記
      • Colima 安裝使用筆記
    • Git Learning

      • Git 版本控制
      • Git 基礎使用 - init, clone, add, commit, status, log
      • Git 分支
      • Git Merge 分支合併指令
      • Git Stash 保存紀錄
      • Git 基礎復原指令 - restore
      • Remote 遠端協同工作 - remote, fetch, pull, push
      • Git Tag 標籤 - tag
      • Git 設定 - config
      • Git 檔案比對與差異 - diff
      • Git Rebase 定義分支的參考基準
      • Subtree 子樹
      • Git Reflog 指令紀錄
      • Git Filter Branch
      • Git Crypt 使用筆記
      • Git 客製化工具
    • Bash

      • Bash 基礎概念
      • Variable 變數
      • Script 腳本撰寫
      • Condition 流程控制
      • Operation 算數表達式
      • Function 函數
      • Command Line Params 指令列參數處理
    • Patterns

      • Introduce
      • Design Patterns

        • Singleton Pattern
        • Compound Pattern
        • Proxy Pattern
        • Hooks Pattern
        • Observer Pattern
        • Mediator/Middleware Pattern
        • HOC Pattern
        • Factory Pattern
        • Render Props Pattern
        • Flyweight Pattern
        • Container/Presentational Component
        • Prototype Pattern
        • Mixin Pattern
        • Provider Component
        • Command Component
        • Module Pattern
      • Render Patterns

        • Rendering Patterns 渲染模式介紹
    • FE 性能優化

      • 前端性能優化
      • 靜態資源優化
      • 頁面渲染架構優化
      • Server 與 Network 優化
      • 開發流程、監控體系優化
    • Ollama 筆記
    • Nginx JS module 基本使用筆記
    • FlowiseAI 使用介紹筆記
    • Github Copilot 使用筆記
    • Traefik Memo
    • API First 學習筆記
    • What is AC - Acceptance Criteria 驗收條件
    • Mermaid 學習筆記
    • 串接 Sonarcube 筆記
    • Youtube Data API 基礎使用筆記
    • FB API 學習筆記
    • VSCode 好用快捷鍵
    • 在 Simulator 上開發測試
    • 問題修正紀錄
  • References

    • 全端開發學習資源
    • 實用工具
    • 網頁前端課程目錄
  • Daily

    • 中醫學習

      • 中醫自學方向
      • 黃帝內經-素問
      • 基礎理論

        • 中藥治病
        • 中藥產地與採收
        • 陰陽五行
        • 精氣血津液
        • 整體觀與臟象
        • 病因病機
        • 辨證論治
        • 四診八綱
        • 五運六氣
        • 經絡
      • 診斷學

        • 辯證
        • 望診
        • 聞診
        • 問診
        • 脈診簡史
        • 切診
        • 常見脈象與臨床意義
        • 經脈辨證
        • 中醫的六邪在疾病初期、後期分別有那些症狀
      • 中藥學

        • 中藥基礎理論
        • 常見中藥材
        • 解表藥(30)
        • 清熱藥(82)
        • 瀉下藥(16)
        • 利水渗湿药(8)
        • 化痰止咳平喘药(15)
        • 止血藥(8)
        • 開竅-平肝-安神藥(11)
        • 化濕藥(5)
        • 祛風濕藥(12)
        • 理氣藥(13)
        • 活血祛瘀藥(7)
        • 消食藥(4)
        • 驅蟲藥(6)
        • 祛寒药(6)
        • 補虛藥(43)
        • 收斂藥(13)
      • 方劑學

        • 總論
        • 解表劑
        • 瀉下劑
        • 清熱劑
        • 理血劑
        • 去暑劑
        • 去濕劑
      • 針灸學

        • 特定穴
      • 其他

        • 內證觀察筆記 上篇【內證漫談】
        • 內證觀察筆記 中篇【太極器官、五藏】
        • 內證觀察筆記 下篇【十二正經】
        • 磁力對生理的影響
    • 易經學習

      • 易經入門
    • 道德經學習

      • 道德經學習重點筆記
      • 第一章 道可道,非常道
      • 第二章 夫唯弗居,是以不去
      • 第三章 不尚賢,使民不爭
      • 第四章 道沖而用之或不盈,淵兮似萬物之宗
      • 第五章 天地不仁,以萬物為芻狗,多聞數窮,不如守中
      • 第六章 谷神不死,是謂玄牝
      • 第七章 天長地久。天地所以能長且久者,以其不自生,故能長生。
      • 第八章 上善若水。水善利萬物而不爭,處眾人之所惡,故幾於道
      • 第九章 功成身退,天之道也。
      • 第十章 載營魄抱一,能無離乎﹖
      • 第十一章 有之以為利,無之以為用
      • 第十二章 五色令人目盲,五音令人耳聾,五味令人口爽,馳騁畋獵令人心發狂,難得之貨令人行妨。
      • 第十三章 寵辱若驚,貴大患若身。愛以身為天下,若可托天下
      • 第十四章 是謂無狀之狀,無物之象,是謂惚恍。迎之不見其首,隨之不見其後。執古之道,以御今之有。
      • 第十五章 保此道者,不欲盈。夫唯不盈,故能蔽而新成。
      • 第十六章 致虛極,守靜篤。萬物並作,吾以觀復。
      • 第十七章 太上,下知有之。悠兮其貴言,功成事遂,百姓皆謂我自然。
      • 第十八章 大道廢,有仁義;智慧出,有大偽;
      • 第十九章 絕聖棄智,民利百倍。見素抱樸,少私寡欲
      • 第二十章 絕學無憂。眾人皆有餘,而我獨若遺。
      • 第二十一章 道之為物,惟恍惟惚。惚兮恍兮,其中有象;恍兮惚兮,其中有物。
      • 第二十二章 曲則全,枉則直,窪則盈,敝則新,少則得,多則惑。是以聖人抱一為天下式
      • 第二十三章 希言自然。天地尚不能久,而況於人乎?信不足焉,有不信焉。
      • 第二十四章 企者不立,跨者不行
      • 第二十五章 有物混成,先天地生。人法地,地法天,天法道,道法自然。
      • 第二十六章 重為輕根,靜為躁君。輕則失本,躁則失君。
      • 第二十七章 善行無轍跡,善言無瑕謫。故善人者,不善人之師;不善人者,善人之資。不貴其師,不愛其資,雖智大迷,是謂要妙。
      • 第二十八章 知雄守雌,為天下谿,知榮守辱,為天下谷
      • 第二十九章 天下神器,不可為也,是以聖人去甚,去奢,去泰。
      • 第三十章 以道佐人主者,不以兵強天下
      • 第三十一章 夫兵者,不祥之器,物或惡之,故有道者不處。
      • 第三十二章 道常無名,樸雖小,天下莫能臣也。
      • 第三十三章 知人者智,自知者明。
      • 第三十四章 大道氾兮,其可左右。以其終不自為大,故能成其大。
      • 第三十五章 執大象,天下往。往而不害,安平太。樂與餌,過客止。
      • 第三十六章 將欲歙之,必固張之;將欲弱之,必固強之
      • 第三十七章 道常無為而無不為
      • 第三十八章 上德不德,是以有德;下德不失德,是以無德
      • 第三十九章 昔之得一者,天得一以清。不欲琭琭如玉,珞珞如石。
      • 第四十章 反者道之動,弱者道之用。天下萬物生於有,有生於無
      • 第四十一章 上士聞道,勤而行之;大方無隅,大器晚成,大音希聲,大象無形,道隱無名。
      • 第四十二章 道生一,一生二,二生三,三生萬物。
      • 第四十三章 天下之至柔,馳騁天下之至堅
      • 第四十四章 名與身孰親﹖身與貨孰多﹖得與亡孰病﹖
      • 第四十五章 大成若缺,其用不弊。大盈若沖,其用不窮。
      • 第四十六章 天下有道,卻走馬以糞。天下無道,戎馬生於郊。
      • 第四十七章 不出戶,知天下;不窺牖,見天道。
      • 第四十八章 為學日益,為道日損。損之又損,以至於無為。
      • 第四十九章 聖人無常心,以百姓心為心。
      • 第五十章 出生入死。生之徒,十有三;死之徒,十有三;人之生,動之死地,亦十有三。
      • 第五十一章 道生之,德畜之,物形之,勢成之。
      • 第五十二章 天下有始,以為天下母。以知其子,既知其子,復守其母,沒身不殆。
      • 第五十三章 使我介然有知,行於大道,唯施是畏。大道甚夷,而人好徑。
      • 第五十四章 善建者不拔,善抱者不脫,子孫以祭祀不輟。
      • 第五十五章 含德之厚,比於赤子。知和曰常,知常曰明。益生曰祥。心使氣曰強。
      • 第五十六章 知者不言,言者不知。
      • 第五十七章 以正治國,以奇用兵,以無事取天下。
      • 第五十八章 其政悶悶,其民淳淳;其政察察,其民缺缺。禍兮福之所倚,福兮禍之所伏。
      • 第五十九章 治人事天,莫若嗇。夫唯嗇,是謂早服
      • 第六十章 治大國,若烹小鮮。夫兩不相傷,故德交歸焉。
      • 第六十一章 大國者下流,天下之交。天下之牝,牝常以靜勝牡,以靜為下。
      • 第六十二章 道者萬物之奧。雖有拱璧以先駟馬,不如坐進此道。
      • 第六十三章 為無為,事無事,味無味。圖難於其易,為大於其細;
      • 第六十四章 合抱之木,生於毫末;九層之臺,起於累土;千里之行,始於足下。為者敗之,執者失之。
      • 第六十五章 古之善為道者,非以明民,將以愚之。
      • 第六十六章 是以欲上民,必以言下之。欲先民,必以身後之。
      • 第六十七章 天下皆謂我道大,似不肖。夫唯大,故似不肖。
      • 第六十八章 善為士者不武,善戰者不怒,善勝敵者不與
      • 第六十九章 吾不敢為主而為客,不敢進寸而退尺。禍莫大於輕敵,輕敵幾喪吾寶。
      • 第七十章 吾言甚易知,甚易行。天下莫能知,莫能行。
      • 第七十一章 知不知上,不知知病。夫唯病病,是以不病。
      • 第七十二章 民不畏威,則大威至。無狎其所居,無厭其所生。夫唯不厭,是以不厭。
      • 第七十三章 天之道,不爭而善勝,不言而善應,不召而自來,繟然而善謀。
      • 第七十四章 民不畏死,奈何以死懼之?若使民常畏死,而為奇者,吾得執而殺之,孰敢?
      • 第七十五章 民之饑,以其上食稅之多,是以饑。
      • 第七十六章 人之生也柔弱,其死也堅強。萬物草木之生也柔脆,其死也枯槁。
      • 第七十七章 天之道,其猶張弓與﹖高者抑之,下者舉之;有餘者損之,不足者補之。
      • 第七十八章 天下莫柔弱於水,而攻堅強者莫之能勝,以其無以易之。
      • 第七十九章 和大怨,必有餘怨,安可以為善﹖
      • 第八十章 小國寡民。使有什伯之器而不用,使民重死而不遠徙。
      • 第八十一章 信言不美,美言不信。天之道,利而不害;聖人之道,為而不爭。
    • 人類真相推廣協會

      • 人類真相介紹
      • 人生大挑戰-閱讀筆記
      • 書籍-人生大挑戰

        • 序文
        • 童年的回憶
        • 賺外快的童年
        • 養家的童年
        • 少年時期的回憶
        • 粉紅睡衣女鬼的祕密
        • 麵線、甘蔗和賽鴿
        • 我在黑社會的日子
        • 『台北一條龍』
        • 賭徒‧妻子‧盤仔人
        • 人鬼之戰—正邪不分的恐怖
        • 開展創業石銅雕畫的日子
        • 渡畜牲者‧瞎掰鬼與邪靈
        • 無所不在的陷阱—邪靈的詭計
        • 鬼屋‧符令‧大揭祕
        • 妖魔鬼怪大變身—邪靈與動物
        • 乩童與宮廟的祕密
        • 活鬼纏身的恐怖亂象
        • 前面親兄弟、後面無情義的假面
        • 前面手牽手、後面下毒手的險境
        • 人心險惡、五馬分屍所逼自殺的真相
        • 自殺後的奇遇—人死後的世界
        • 我自殺後的奇遇—「陰間地府處」與「陰府大本營」
        • 我被趕出家門、面臨眾叛親離的苦境
        • 其他
    • 英文學習

      • 自我介紹
      • 簡單寒暄
      • 指路
      • 買賣
      • 轉接電話
      • 通勤
      • 訂位
      • 數字
    • 金剛經 時間不存在?自我是虛擬?
    • 20250319 外婆過世
    • 說服的藝術:避免陷阱,建立共識
    • 禪修一定要去除「如何」嗎?
    • 不對稱的回報
    • 避免慢性壓力
    • 關鍵溝通表達力
    • 快樂的秘訣
    • 生活在現代社會,維持高品質思考的重要性
    • 心理學家的面相術:解讀情緒的密碼
    • 什麼是真相,真相是什麼
    • 身為一位歷史觀察者(文長慎入)
    • 2022 Leisure Learn
    • 兩年八個月,我從 Garena 畢業了
    • DeFi
    • 我的終生大事,交往到結婚
    • 轉職前端工程師 3 年多的回顧
    • MacOS + Iterm2 + Oh My Zsh
    • 我喜歡的名言佳句
    • Web Interview Preparation

NodeJS 輕量開發框架 Expressjs 與 Koa2 的區別

tags: JS Nodejs Expressjs Koa2

Share On:

FacebookLINEMessengerTelegram

相信 Nodejs 小粉絲一定很好奇,到底 Koa2 跟 Express 差在哪?為何尤雨溪會選擇 Koa2 作為 Vite 的 server 使用而不是廣為人知的老大哥 Express 呢?

今天這篇就來一探究竟 Express 與 Koa2 有什麼區別吧!(這篇只是客觀分析喔~大家別戰起來!)

市場定位

要了解兩個工具之間的區別,最重要的是先了解他們的目標定位,就來看看各自官網怎麼描述吧

先來看一下 Express

  1. 最小又靈活的 Node.js Web 應用程式架構
  2. 大量的 HTTP 公用程式方法與中介軟體供您支配,能夠快速又輕鬆的建立完整的 API。
  3. 提供精簡的基礎 Web 應用程式特性,又不使您所認識及喜好的 Node.js 失色

再來是 Koa

  1. 由原班 Express 開發團隊打造的新一代網頁開發框架
  2. 目標打造一個更輕量且高效穩定的網頁應用程式與 API 開發工具
  3. async, await 取代傳統回調,大大增加錯誤處理能力
  4. Koa 沒有在其核心中捆綁任何中間件,提供了一套優雅的方法,使編寫服務器變得快速而愉快

總結來說,Koa 的某些定位與 Express 非常接近,都是一款輕量、高效的框架,畢竟開發團隊就是同一群人,但 Koa 顯然是針對 Express 的一些弱項進行優化後的版本,所以某些方面來說,Express 更貼近原生 Nodejs,專注處理 route 的部分,Koa 則類似於精簡優化過的 Express,但也因此 Koa 的 API 比起 Express 而言,與原生 Nodejs API 有一些差別

有關 Koa 的第四點,是因為過去 Express即使沒有添加任何 middleware,內部就會預設有兩個中間件了,對於「輕量」這一點來說,Koa 完全沒有多餘的中間件負擔,但對於抽象 API 這一點來說,Express 則較為貼近原生 Nodejs

建立 Server

對於一些基礎 api 除了寫法上的差別外,整體概念是非常像的

// express
const express = require('express');
const app = express();

app.use((req, res, next) => {
  res.send('Hello');
});

app.listen(3000);
// koa
const Koa = require('koa');
const app = new Koa();

app.use((ctx, next) => {
  ctx.body = 'Hello';
});

app.listen(3000);

Request/Response

在 Express 中
request, response 是獨立開來分別給開發者使用的,並且 req, res 僅僅是原生 Nodejs 的 HTTP 物件包裝,所以可以直接調用原生 method 都沒問題,根據官網的解釋

The req object is an enhanced version of Node’s own request object and supports all built-in fields and methods.

在 Koa 中
Koa 將 request, response 放在內部維護的 ctx 中,每一次請求都會產生一個全新的 ctx 物件,為了方便使用,ctx 內許多 key 會直接幫助開發者指向正確的位置,比如說 ctx.get, ctx.set 分別代理的是 ctx.request.get, ctx.response.set,詳細可以參考官網

但因為 Koa 是對於原生 HTTP 物件進行抽象的關係,使用原生像是 ctx.response.setHeader 就會出現錯誤,而 Express 則可以正確執行 res.setHeader。

設置 logger

想要設定一個 logger 時,乍看用到的 API 其實是非常相似的,但可以很明顯看出 Express, Koa 在 middleware 這一塊裡有個明顯的差異,導致 Express 在做這種功能時綁手綁腳

// Express
const express = require('express');
const app = express();

// logger

app.use((req, res, next) => {
  next();
  const rt = res.get('X-Response-Time');
  console.log(`${req.method} ${req.url} - ${rt}`);
});

// x-response-time

app.use((req, res, next) => {
  const start = Date.now();
  next();
  const ms = Date.now() - start;
  // 這邊會噴錯,因為 next 已經 res.send 完畢
  // Cannot set headers after they are sent to the client
  res.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async (req, res) => {
  res.send('Hello World');
});

app.listen(3000);
// Koa
const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  // ctx.set is alias for ctx.response.set
  // 這裡沒問題,ctx.body 不會阻斷這邊繼續執行
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

在 Express 中,如果呼叫了 res.send,則請求到該 middleware就會將 header 送出,而在 Koa 中,即使呼叫了 ctx.body,後續 middleware 中還是可以對 header 進行調整修正,因為實際上 Koa 還沒有真正返回請求,一直到所有 middleware 都依序執行完畢才會返回

那到底在 Express 中我們要怎麼做到 response duration 的功能呢?必須借助套件 on-headers 的幫助,監聽 write header 的動作,並在 write 之前進行計算

const express = require('express');
const onHeaders = require('on-headers');
const app = express();

// logger

app.use((req, res, next) => {
  next();
  const rt = res.get('X-Response-Time');
  console.log(`${req.method} ${req.url} - ${rt}`);
});

// x-response-time

app.use((req, res, next) => {
  const startAt = new Date().getTime();

  onHeaders(res, () => {
    const diff = new Date().getTime();
    var time = diff - startAt;
    if (res.get('X-Response-Time')) return;
    res.set('X-Response-Time', `${time}ms`);
  });

  next();
});

// response

app.use(async (req, res) => {
  res.send('Hello World');
});

app.listen(3000);

是不是覺得有點矇啊...,為了一個感覺很基本的功能居然要這樣大費周章,當然你要存全域變數去做也是可以拉

Middleware 中間件

中間件算是 Express 的某些情況下的劣勢,具體比較是怎麼樣呢,我們來看看

Express 同步

首先拿一個正常情況下的 express 中間件情況

const express = require('express');
const app = express();

app.use((req, res, next) => {
  console.log('1st middleware start');
  next();
  console.log('1st middleware end');
});

app.use((req, res, next) => {
  console.log('2nd middleware start');
  next();
  console.log('2nd middleware end');
});

app.get('/api/test', (req, res) => {
  console.log('trigger /api/test');
  res.status(200).send('hello')
});

app.listen(3000);

RESULT

上面對 /api/test 請求時會得出下面的結果,符合預期,下面接著看看非同步時的結果

1st middleware start
2nd middleware start
trigger /api/test
2nd middleware end
1st middleware end

Express 非同步

稍微修改下上面的,加入了 sleep 的非同步等待

const express = require('express');
const app = express();

const sleep = (ms) => new Promise((res) => setTimeout(() => {
  console.log('sleep timeout...');
  res();
}, ms));

app.use(async (req, res, next) => {
  console.log('1st middleware start');
  await next();
  console.log('1st middleware end');
});

app.use(async (req, res, next) => {
  console.log('2nd middleware start');
  await next();
  console.log('2nd middleware end');
});

app.get('/api/test', async (req, res) => {
  console.log('trigger /api/test');
  await sleep(2000); // 加入非同步的等待
  res.status(200).send('hello');
});

app.listen(3000);

RESULT

可以看到即使我們幫前幾個 middleware 的 next 加上 await,但這結果明顯亂了套,我們預期的是 sleep 後才跑到 2nd, 1st 中間件的 end,結果卻是其他後續 middleware 被提前調用了,原因是 express 的 next 函數並沒有包裝為 Promise,也就不支援 await

雖然這不影響最終返回的結果,但對於某些會被執行順序影響的結果而言就會有影響了,比如上面那種紀錄 response duration 的中間件,面對非同步請求時如果沒有妥善處理就會導致一些問題

1st middleware start
2nd middleware start
trigger /api/test
2nd middleware end
1st middleware end
sleep timeout...

Express 中間件邏輯

會發生上面這種怪事,相信眼尖的人應該已經知道為什麼了,這主要是 express 的中間件在當初設計上的一個小瑕疵,上面的中間件經過拆解後實際會像下面這樣執行,next 函數的設計是關鍵

((req, res) => {
  console.log('1st middleware start');
  ((req, res) => {
    console.log('2nd middleware start');
    // 可以看到,這裡的非同步函數是被包裹起來執行的,也因此後續的中間件行為並不會等待他的結果返回
    // 且因為 next 沒有被 Promise 包裹
    (async(req, res) => {
      console.log('trigger /api/test');
      await sleep(2000);
      res.status(200).send('hello')
    })(req, res)
    console.log('2nd middleware end');
  })(req, res)
  console.log('1st middleware end');
})(req, res)

相信看到這大家就能理解為何會出現像上面那種奇怪的打印結果了吧~,其實當初 express 會這麼設計也不是不能理解,畢竟設計當時應該還無法遇見現在 async, await 這麼流行吧

接下來我們看看新的小老弟 Koa2 是如何優雅地處理中間件的吧

Koa2 中間件邏輯

Koa2 處理中間件邏輯的部分是一個獨立的包koa-compose,其實就只有一個檔案在處理,真的是非常精煉...下面一起看看他的源碼怎麼做吧

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)

    // dispatch 返回都是 Promise
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      // 完成遍歷時,自動改為調用 next
      if (i === middleware.length) fn = next
      // fn 不存在時,完成任務,返回 Promise
      if (!fn) return Promise.resolve()
      // 掛載 Promise 執行,傳入執行下一個 middleware 的 next dispatch
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

可以看到 Koa2 的 compose 就是透過閉包和遞迴的性質來一層一層處理 middleware,且每一個 next 都是返回 Promise,也就直接支援進行 await 處理

結論

總結來說,Express 與 Koa2 的差別在於,對基礎開發 API 設計的方式一個更貼近原生,一個更講究方便,並且在 middleware 的處理上,Express 屬於直線執行,Koa2 則採用 stack 的洋蔥執行方式,以目前主流 async, await 來說,Express 也是可以使用,只是在某些場景下會顯得比較棘手,不知道大家在瞭解了兩者的差別後,有什麼想法呢?

不管怎樣,最重要的還是開發者自由心證拉~,希望大家都能找到自己最愛的開發框架摟,畢竟開發就是用自己最擅長,最喜歡的工具就對了=V=,今天寫到這了,感謝大家觀看,覺得內容不錯也歡迎分享出去讓更多人看看吧

Share On:

FacebookLINEMessengerTelegram

參考

  • 再也不怕面试官问你express和koa的区别了
  • Koajs 官網
  • Expressjs 官網
  • Expressjs response-time source code
最近更新:: 2022/3/29 凌晨2:43
Contributors: johnnywang1994
Prev
手寫一個可中斷的 delay promise
Next
用 2D 物理引擎 Matterjs 製作經典馬力歐 1-1