Koa 是運行在 Node.js 中的 web 服務框架,小而美。html
Koa2 是 Koa 框架的最新版本,Koa3 尚未正式推出,Koa1 正走在被替換的路上。前端
Koa2 與 Koa1 的最大不一樣,在於 Koa1 基於 co 管理 Promise/Generator 中間件,而 Koa2 緊跟最新的 ES 規範,支持到了 Async Function(Koa1 不支持),二者中間件模型表現一致,只是語法底層不一樣。node
Koa2 正在蠶食 Express 的市場份額,最大的緣由是 Javascript 的語言特性進化,以及 Chrome V8 引擎的升級,賦予了 Node.js 更大的能力,提高開發者的編程體驗,知足開發者靈活定製的場景以及對於性能提高的需求,蠶食也就水到渠成,2018 年開始,Koa2 會超越 Express 成爲本年最大普及量的 Node.js 框架。程序員
以上就是 Koa2 的現狀,以及它的趨勢,站在 2018 年的節點來看,Koa2 的學習大潮已經到來,那麼若是要掌握 Koa2,須要去學習它的哪些知識呢,這些知識跟 Node.js 以及語言規範有什麼關係,它的內部組成是如何的,運行機制怎樣,定製拓展是否困難,以及它的三方庫生態如何,應用場景有哪些,跟前端有如何結合等等,這些問題本文將作簡要的探討,Koa2 詳細的代碼案例和深度剖析見這裏 。web
備註:以下提到的 Koa 均指代 Koa 2.x 版本
瞭解過 TJ 的童鞋都知道,他以驚爲天人的代碼貢獻速度、源源不斷的開發熱情和巧奪天工的編程模型而推進整個 Node.js/NPM 社區大步邁進,稱爲大神絕不過度,而大神的腦回路,向來與凡人不一樣。數據庫
關於大神的傳說有不少,最有意思的是在國外著名程序員論壇 reddit 上,有人說,TJ 歷來就不是一我的,一我的能有這麼高效而瘋狂的代碼產出實在是太讓人震驚了,他背後必定是一個團隊,由於他歷來都不參加技術會議,也不見任何人,而最後 TJ 離開 Node 社區去轉向 Go,這種作事方式很是谷歌,因此 TJ 是谷歌的一個招牌,你們衆說紛紜,吵的不可開交,不過有一點你們都是達成共識的,那就是很是確定和感謝他對於 Nodejs 社區的貢獻和付出。express
聊 Koa 以前,先對比下 Express,在 Express 裏面,不一樣時期的代碼組織方式雖然大爲不一樣,好比早期是全家桶各類路由、表單解析都囊括到一個項目中,中後期作了大量的拆分,將大部分模塊都獨立出來官方自行維護,或者是採用社區其餘開發者提供的中間件模塊,但縱觀 Express 多年的歷程,他依然是相對大而全,API 較爲豐富的框架,而且它的整個中間件模型是基於 callback 回調,而 callback 常年被人詬病。編程
對於一個 web 服務框架來講,它的核心流程,就是在整個 HTTP 進入到流出的過程當中,從它的流入數據上採集所須要的參數素材,再向流出的數據結構上附加指望素材,不管是一個靜態文件仍是 JSON 數據,而在採集和附加的過程當中,須要各個中間件大佬的參與,有的乾的是記錄日誌的活兒,有的乾的是解析表單的活兒,有的則是管理會話,既然是大佬,通常都脾氣大,你不安排好他們的註冊順序,不經過一種機制管理他們的入場退場順序,他們不只很差好配合,還可能砸了你的場子。api
那麼 Express 裏面,首先就是對於 HTTP 這個你們夥的管理(其餘協議先不涉及),管理這個你們夥,Express 祭出了三件,哦不,實際上是四件法寶。
首先是經過 express() 拿到的整個服務器運行實例,這個實例至關因而一個酒店,而你就是來訪的客人 - HTTP 請求,酒店負責你一切需求,作到你滿意。
在酒店裏面,還有兩個工做人員,一個是 req(request) 負責接待你的叫阿來吧,還有一個送你離開的狠角色 - res(response),叫阿去吧,阿來接待到你進酒店,門口的攝像頭會你拍照(Log 記錄來去時間,你的特徵),收集你的指紋(老會員識別),引領你去前臺簽到(獲取你的需求,好比你要拿走屬於你的一套西服),而後酒店安排你到房間休息(等待響應),裏面各類後勤人員忙忙碌碌接待不一樣的客人,其中有一個是幫你取西服的,取了後,交給阿來,阿來再把西服穿你身上,同時還可能幫你裝飾一番,好比給你帶個帽子(加個自定義頭),而後送你出門,門口的攝像頭還會拍你一下,就知道了酒店服務你的時間......實在編不下去了,想用物理世界的案例來對應到程序世界是蠻難的,嚴謹度不夠,不過幫新手同窗留下一個深入印象卻是可取的。數組
上面酒店的 4 件法寶,其實就是服務器運行實例,req 請求對象,res 響應對象和中間件 middlewares,剛纔負責照相的,簽到的,分析需求的其實都是中間件,一個一個濾過去,他們根據本身的規則進行採集、分析、轉化和附加,把這個 HTTP 客人,從頭到腳捏一遍,客人就舒舒服服的離開了。
中間件是衆多 web 框架中比較核心的概念,它們能夠根據不一樣的場景,來集成到框架中,加強框架的服務能力,而框架則須要提供一套機制來保證中間件是有序執行,這個機制在不一樣的框架中則大爲不一樣,在 Express 裏面,咱們經過 use(middlewares()) 逐個 use 下去,use 的順序和規則都由 express 自身控制。
在 express/express.js 中,服務器運行實例 app 經過 handle 來把 Nodejs 的 req 和 res 傳遞給 handle 函數,賦予 handle 對於內部對象的控制權:
app = function(req, res, next) { app.handle(req, res, next) }
而在 express/application.js 中,拿到控制權的 handle 又把請求響應和回調,繼續分派給了 express 的核心路由模塊,也就是 router:
app.handle = function handle (req, res, callback) { var router = this._router var done = callback || finalhandler(req, res, { env: this.get('env'), onerror: logerror.bind(this) }) router.handle(req, res, done) }
這裏的 router.handle 就持有到了 req, res 對象,能夠理解爲,express 把 Nodejs 監聽到的請求三要素(req, res, cb) 下放給了內部的路由模塊 router。
而後繼續回到剛纔 use(middlewares(),Express 每一次 use 中間件,都會把這個中間件也交給 router:
app.use = function use(fn) { router.use('/', fn) }
而 router 裏面,有很重要一個概念,就是 layer 層,能夠理解爲中間件堆疊的層,一層層堆疊起來:
var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, fn) this.stack.push(layer)
以上是僞代碼(刪減了大部分),能夠看作是 express 在啓動運行的時候,註冊好了一箇中間件函數棧,裏面堆疊好了待被調用的中間件,一旦請求進來,就會被 router handle 來處理:
proto.handle = function handle(req, res, out) { next() function next(err) { var layer var route self.process_params(layer, paramcalled, req, res, function (err) { if (route) { return layer.handle_request(req, res, next) } trim_prefix(layer, layerError, layerPath, path) }) } function trim_prefix(layer, layerError, layerPath, path) { if (layerError) { layer.handle_error(layerError, req, res, next) } else { layer.handle_request(req, res, next) } } }
handle 裏面的 next 是整個中間件棧可以轉起來的關鍵,在全部的中間件裏面,都要執行這個 next,從而把當前的控制權以回調的方式往下面傳遞。
可是問題就是這種機制在最初的時候,若是沒有事件的配合,是很難作到原路進去,再順着原路回去,至關因而每一箇中間件都被來回濾了 2 遍,賦予中間件更靈活的控制權,這就是掣肘 Express 的地方,也是 Express 市場必定會被 Koa 蠶食的重要緣由。
具體 Express 的代碼比這裏描述的要複雜好幾倍,你們有興趣能夠去看源碼,應該會有更多的收穫,若是沒有 Koa 這種框架存在的話,Express 的內部實現用精妙形容絕對不爲過,只是這種相對複雜一些的內部中間件機制,未必符合全部人的口味,也說明了早些年限於 JS 的能力,想要作一些流程雙向控制多麼困難。
關於 Express 就分析到這裏,這不是本文的重點,瞭解它內部的複雜度以及精妙而複雜都實現就能夠了,由於這是特定歷史階段的歷史產物,有它特定的歷史使命。
得益於大神非同尋常的腦回路,Koa 從一開始就選擇了跟 Express 徹底不一樣的架構方向,上面 Express 的部分你們沒看懂也不要緊,由於 Koa 這裏的處理,會讓你瞬間腦回路清晰。
首先要明白,Koa 與 Express 是在作一樣事情上的不一樣實現,因此意味着他倆對外提供的能力大部分是相同的,這部分不贅述,咱們看不一樣的地方:
Koa 內部也有幾個神行太保,能力較大,首先 new Koa() 出來的服務器運行實例,它像青蛙同樣,張大嘴吞食全部的請求,經過它能夠把服務真正跑起來,跟 Express 同樣,這個就跳過不提了,重點是它的 context,也就是 ctx,這貨上面有不少引用,最核心的是 request 和 response,這倆能夠對應到 Express 兩個對立的 req 和 res,在 Koa 裏面,把它倆都集中到 ctx 裏面進行管理,分別經過 ctx.request 和 ctx.reponse 進行直接訪問,原來 Express 兩個獨立對象作的事情,如今一個 ctx 就夠了,上下文對象都在他手中,想要聯繫誰就能聯繫誰。
其次是它的中間件機制,Koa 真正的魅力所在,先看段代碼:
const Koa = require('koa') const app = new Koa() const indent = (n) => new Array(n).join(' ') const mid1 = () => async (ctx, next) => { ctx.body = `<h3>請求 => 第一層中間件</h3>` await next() ctx.body += `<h3>響應 <= 第一層中間件</h3>` } const mid2 = () => async (ctx, next) => { ctx.body += `<h3>${indent(4)}請求 => 第二層中間件</h3>` await next() ctx.body += `<h3>${indent(4)}響應 <= 第二層中間件</h3>` } app.use(mid1()) app.use(mid2()) app.use(async (ctx, next) => { ctx.body += `<p style="color: #f60">${indent(12)}=> Koa 核心 處理業務 <=</p>` }) app.listen(2333)
你們能夠把這 22 行代碼跑起來,瀏覽器裏訪問 localhost:2333 就能看到代碼的執行路徑,一個 HTTP 請求,從進入到流出,是兩次穿透,每個中間件都被穿透兩次,這個按照次序的正向進入和反向穿透並非必選項,而是 Koa 輕鬆具有的能力,一樣的能力,在 Express 裏面實現反而很費勁。
想要了解上面提到的能力,就要看下 Koa 核心的代碼:
一樣是 app.use(middlewares()),在 koa/application.js 裏面,每個中間件一樣被壓入到一個數組中:
use(fn) { this.middleware.push(fn) }
在服務器啓動的時候,創建監聽,同時註冊回調函數:
listen(...args) { server = http.createServer(this.callback()).listen(...args) }
回調函數裏面,返回了 (req, res) 給 Node.js 用來接收請求,在它內部,首先基於 req, res 建立出來 ctx,就是那個同時能管理 request 和 response 的傢伙,重點是上面壓到數組裏面的 middlewares 被 compose 處理後,就扔給了 handleRequest:
callback() { const fn = compose(this.middleware) return handleRequest = (req, res) => { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } }
compose 就是 koa-compose,簡單理解爲經過它,以遞歸的方式實現了 Promise 的鏈式執行,由於咱們都知道, async function 本質上會返回一個 Promise,這裏 compose 跳過不說了,繼續去看 handleRequest:
handleRequest(ctx, fnMiddleware) { return fnMiddleware(ctx).then(respond(ctx)) }
實在是簡潔的不像實力派,請求進來後,會把能夠遞歸調用的中間件數組都執行一遍,每一箇中間件都能拿到 ctx,同時,由於 async function 的語法特性,能夠中間件中,把執行權交給後面的中間件,這樣逐層逐層交出去,最後再逐層逐層執行回來,就達到了請求沿着一條路進入,響應沿着一樣的一條路反向返回的效果。
借用官方文檔的一張圖來表達這個過程:
我知道這張圖還不夠,再祭出官方的第二張圖,著名的洋蔥模型:
從上面的對比,咱們其實就發現了 Koa2 獨具魅力的地方,這些魅力一方面跟框架設計理念有關,一方面跟語言特性有關,語言特性,無外乎下面幾個:
這些都是基礎性的值得學習的,這些知識跟着語言規範有着很是親近的關係,因此意味着學會這些之後,也須要去到 ES6/7/8 裏面挑選更多的語法特性,早早入坑學習,限於篇幅本文均再也不探討,上面的基礎知識學習若是有興趣,能夠跟着 Koa2解讀+數據抓取+實戰電影網站 瞭解更多實戰姿式。
能不能來個痛快話?其實能夠的,選 Koa2 吧,2018 年了,不用等了。
同時必定非它不可麼,其實也不是,咱們能夠更加客觀的看待選擇問題,再梳理下思緒:
Koa 是基於新的語法特性,實現了 Promise 鏈傳遞,錯誤處理更友好,Koa 不綁定任何中間件,是乾乾淨淨的裸框架,須要什麼就加什麼,Koa 對流支持度很好,經過上下文對象的交叉引用讓內部流程與請求和響應串聯的更緊湊,若是 Express 是大而全,那麼 Koa 就是小而精,兩者定位不一樣,只不過 Koa 擴展性很是好,稍微組裝幾個中間件立刻就能跟 Express 匹敵,代碼質量也更高,設計理念更先進,語法特性也更超前。
這是站在用戶的角度比較的結果,若是站在內部實現的角度,Koa 的中間件加載和執行機制跟 Express 是大相徑庭的,他倆在這一點上的巨大差異也致使了一個項目能夠徹底走向兩種不一樣的中間件設計和實現方式,不過每每咱們是做爲框架的使用者,業務的開發者來使用的,那麼對於 Nodejs 的用戶來講,Express 能知足你的,Koa 均可以知足你,Express 讓你爽的,Koa 可讓你更爽。
這也是爲何,阿里的企業級框架 Eggjs 底層是 Koa 而不是 Express,360 公司的大而全的 thinkjs 底層也是 Koa,包括沃爾瑪的 hapi 雖然沒有用 Koa,可是他的核心開發者寫博客說,受到 Koa 的衝擊和影響, 也要升級到 async function,保持對語法的跟進,而這些都是 Koa 已經作好了整個底子,任何上層架構變得更簡單了。
你們在選用 Express 的時候,或者從 Express 升級到 Koa 的時候,其實不用太糾結,只要成本容許,均可以使用,若是實現成本太高,那麼用 Express 也沒問題的,遇到其餘新項目的時候,沒有了歷史包袱,在用 Koa 也不遲。
其實經過上面的篇幅,咱們對於內部組成基本瞭解了,運行機制其實就是中間件執行機制,而定製拓展性,咱們上面提到了 Eggjs 和 Thinkjs 已經充分證實了它可定製的強大潛力,這裏咱們主要聊下跟運行機制相關的,一個是 Koajs 自身,另外的一個是經過它向下到 Node.js 底層,它的運行機制是怎樣的,這塊涉及到 Libuv 的事件循環,若是不瞭解的話,很難在 Node.js 這顆技能樹上再進一臺階,因此它也很是重要。
而 Libuv 的事件循環,本質上決定了 Node.js 的異步屬性和異步能力,提到異步,咱們都知道 Node.js 的異步非阻塞 IO,可是你們對於 同步異步以及阻塞非阻塞,都有了本身的理解,說到異步 IO,其實每每咱們說的是操做系統所提供的異步 IO 能力,那首先什麼是 IO,說白了,就是數據進出,人機交互的時候,咱們會把鍵盤鼠標這些外設看作是 Input,也就是輸入,對應到主機上,會有專門流入數據或者信號的物理接口,顯示器做爲一個可視化的外設,對應到主機上,會有專門的輸出數據的接口,這就是生活中咱們可見的 IO 能力,這個接口再向下,會進入到操做系統這個層面,在操做系統層面,會提供諸多的能力,好比磁盤讀寫,DNS 查詢,數據庫鏈接,網絡請求接收與返回等等,在不一樣的操做系統中,他們表現出來的特徵也不一致,有的是純異步的,非阻塞的,有的是同步的阻塞的,不管怎麼樣,咱們均可以把這些 IO 看作是上層應用和下層系統之間的數據交互,上層依賴於下層,上層也能夠進一步對這些能力進行定製改造,若是這個交互是異步的非阻塞的,那麼這種就是 異步 IO 模型,若是是同步的阻塞的,那麼就是同步 IO 模型。
在 Nodejs 裏面,咱們能夠拿文件讀寫爲例,Koa 只是一個上層的 web 應用服務框架而已,它全部與操做系統之家的溝通能力,都創建在 Node.js 整個的通訊服務模型的基礎之上,Nodejs 提供了 filesystem 也就是 fs 這個模塊,模塊中提供了文件讀寫的接口,好比 readFile 這個異步的接口,它就是一個典型的異步 IO 接口,反之 readFileSync 就是一個阻塞的同步 IO 接口,以這個來類推,咱們站在上層的 web 服務這個層面,就很容易理解 Node.js 的異步非阻塞模型,異步 IO 能力。
那麼 Node.js 的異步能力又是創建在 Libuv 這一層的幾個階段上的,什麼?還有階段?
是的,Node.js 的底層除了解釋和執行 JS 代碼的 Chrome V8 虛擬機,還有一大趴兒就是 Libuv,它跟操做系統交互,封裝了不一樣平臺的諸多接口,至關於抹平了操做系統的異步差別帶來的兼容性,讓 Node.js 對外提供一致的同異步 API,而 Libuv 的幾個階段,即是對於單線程的 JS 最有利的輔助實現,全部的異步均可以看作是任務,任務是耗時的,libuv 把這些任務分紅不一樣類型,分到不一樣階段,有他們各自的執行規律和執行優先級。
你們能夠先預測下下面這段代碼的執行結果:
const EventEmitter = require('events') class EE extends EventEmitter {} const yy = new EE() yy.on('event', () => console.log('粗大事啦')) setTimeout(() => console.log('0 毫秒後到期的定時器回調'), 0) setTimeout(() => console.log('100 毫秒後到期的定時器回調'), 100) setImmediate(() => console.log('immediate 當即回調')) process.nextTick(() => console.log('process.nextTick 的回調')) Promise.resolve().then(() => { yy.emit('event') process.nextTick(() => console.log('process.nextTick 的回調')) console.log('promise 第一次回調') }) .then(() => console.log('promise 第二次回調'))
你會發現你踏入了一個 【美好】 的世界,這就是咱們經過了解 Koa 之後,若是想要繼續往下學習,須要掌握的知識,這塊知識才是真正的乾貨,一言半語的確說不清楚,咱們保留思路往下走。
在 Koa1 時代和 Koa2 剛出的時候,的確它的三方庫很少,須要本身動手包裝,甚至還有 koa-convert 專門幹這個活兒,把 1 代 koa 中間件轉成能夠兼容 2 代 koa 能夠兼容的形式。
可是時至今日,Koa2 的生態已經至關完善了,尤爲在 2018 年隨着更多開發者切入到 Koa2 中,將會有大批量的業界優秀模塊庫進入到 Koa2 的大池子中,你們會發現可選擇的愈來愈多,因此他的生態沒問題。
到這裏,本文接近尾聲了,我也感受意猶未盡,可是再寫下去怕是成飛流直下三千尺了,我想用一句話回答這個問題:
小而美是每個工程師最終會選擇自我修養,Koa2 是小而美的,能與它結合的必然也是小而美的,那麼在 2018 年,就非 Parcel 莫屬,小而美絕配,關於 Parcel 如何 AntDesign/React/Bootstrap 等這些前端框架庫組合使用,能夠關注 Koa2解讀+數據抓取+實戰電影網站 瞭解更多姿式。
回到本文的標題:Koa2 還有多久取代 Express?我想徹底替代是不可能的,可是新項目使用 Koa2(以及基於它封裝的框架)將會在數量上碾壓 Express,時間呢,2018 - 2019 兩年足矣,那麼 2018 年起,但求不落後,加油!
封面圖來自 codetrick