koa2的特點優勢
什么是 koa2
- Nodejs官方api支持的都是callback形式的異步編程模型。問題:callback嵌套問題
- koa2 是由 Express原班人馬打造的,是現在比較流行的基于Node.js平臺的web開發框架,Koa 把 Express 中內置的 router、view 等功能都移除了,使得框架本身更輕量,而且擴展性很強。使用koa編寫web應用,可以免除重復繁瑣的回調函數。
koa2 的優點
優點這個東西,我直接說它多好,你可能又不開心,但是我們可以對比哦!這里我只說它對比原生的 Node.js開啟 http 服務 帶來了哪些優點!
- 先看一下原生 Node.js 我開啟一個 http 服務
const http = require('http');http.createServer((req,res)=>{ res.writeHead(200); res.end('hi koala');}).listen(3000);
- 看一下使用 koa2 開啟一個http 服務
const Koa = require('koa') ;const app = new Koa();const {createReadStream} = require('fs');app.use(async (ctx,next)=>{ if(ctx.path === '/favicon.ico'){ ctx.body = createReadStream('./avicon.ico') }else{ await next(); }});app.use(ctx=>{ ctx.body = 'hi koala';})app.listen(3000);
我在 koa2 中添加了一個判斷 /favicon.ico 的實現 通過以上兩段代碼,會發現下面幾個優點
- 傳統的 http 服務想使用模塊化不是很方便,我們不能在一個服務里面判斷所有的請求和一些內容。而 koa2 對模塊化提供了更好的幫助
- koa2 把 req,res 封裝到了 context 中,更簡潔而且方便記憶
- 中間件機制,采用洋蔥模型,洋蔥模型流程記住一點(洋蔥是先從皮到心,然后從心到皮),通過洋蔥模型把代碼流程化,讓流水線更加清楚,如果不使用中間件,在 createServer 一條線判斷所有邏輯確實不好。
- 看不到的優點也很多,error 錯誤處理,res的封裝處理等。
自己實現一個koa2
在實現的過程中會我看看可以學到那些知識
listen 函數簡單封裝
koa2 直接使用的時候,我們通過 const app = new Koa();,koa 應該是一個類,而且可以直接調用 listen 函數,并且沒有暴漏出http 服務的創建,說明在listen函數中可能創建了服務。到此簡單代碼實現應該是這樣的:
class Kkb{ constructor(){ this.middlewares = []; } listen(...args){ http.createServer(async (req,res)=>{ // 給用戶返回信息 this.callback(req,res); res.writeHead(200); res.statusCode = 200; res.end('hello koala') }).listen(...args) }}module.exports = Kkb;
實現 context 的封裝
實現了簡單 listen 后,會發現回調函數返回的還是 req 和 res ,要是將二者封裝到 context 一次返回就更好了!我們繼續
const ctx = this.createContext(req,res);
看一下 createContext 的具體實現
const request = require('./lib/request');const response = require('./lib/response');const context = require('./lib/context'); createContext(req,res){ // 創建一個新對象,繼承導入的context const ctx = Object.create(context); ctx.request = Object.create(request); ctx.response = Object.create(response); // 這里的兩等于判斷,讓使用者既可以直接使用ctx,也可以使用原生的內容 ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; }
context.js
module.exports = { get url(){ return this.request.url; }, get body(){ return this.response.body; }, set body(val){ this.response.body = val; }}
request.js
module.exports = { get url(){ return this.req.url; }}
這里在寫 context.js 時候,用到了set 與 get 函數,get 語句作為函數綁定在對象的屬性上,當訪問該屬性時調用該函數。set 語法可以將一個函數綁定在當前對象的指定屬性上,當那個屬性被賦值時,你所綁定的函數就會被調用。
實現洋蔥模型
compose 另一個應用場景
說洋蔥模型之前先看一個函數式編程內容:compose 函數前端用過 redux 的同學肯定都很熟悉。redux 通過compose來處理 中間件 。原理是 借助數組的 reduce 對數組的參數進行迭代
// redux 中的 compose 函數export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args)))}
洋蔥模型實現
再看文章開頭 koa2 創建 http 服務函數,會發現多次調用 use 函數,其實這就是洋蔥模型的應用。
洋蔥是由很多層組成的,你可以把每個中間件看作洋蔥里的一層,根據app.use的調用順序中間件由外層到里層組成了整個洋蔥,整個中間件執行過程相當于由外到內再到外地穿透整個洋蔥
引用一張著名的洋蔥模型圖:

每次執行 use 函數,我們實際是往一個函數數組中添加了一個函數,然后再次通過一個 compose 函數,處理添加進來函數的執行順序,也就是這個 compose 函數實現了洋蔥模型機制。
具體代碼實現如下:
// 其中包含一個遞歸 compose(middlewares){ return async function(ctx){// 傳入上下文 return dispatch(0); function dispatch(i){ let fn = middlewares[i]; if(!fn){ return Promise.resolve(); } return Promise.resolve( fn(ctx,function next(){ return dispatch(i+1) }) ) } } }
首先執行一次 dispatch(0) 也就是默認返回第一個 app.use 傳入的函數 使用 Promise 函數封裝返回,其中第一個參數是我們常用的 ctx,
第二個參數就是 next 參數,next 每次執行之后都會等于下一個中間件函數,如果下一個中間件函數不為真則返回一個成功的 Promise。因此我們每次調用 next() 就是在執行下一個中間件函數。
來試試我們自己實現的koa2
使用一下我們自己的 koa2 吧,用它做一道常考洋蔥模型面試題,我想文章如果懂了,輸出結果應該不會錯了,自己試一下!
const KKB = require('./kkb');const app = new KKB();app.use(async (ctx,next)=>{ ctx.body = '1'; await next(); ctx.body += '3';})app.use(async (ctx,next)=>{ ctx.body += '4'; await delay(); await next(); ctx.body += '5';})app.use(async (ctx,next)=>{ ctx.body += '6'})async function delay(){ return new Promise((reslove,reject)=>{ setTimeout(()=>{ reslove(); },1000); })}app.listen(3000);
解題思路:還是洋蔥思想,洋蔥是先從皮到心,然后從心到皮
答案: 1 4 6 5 3
補充與說明
本文目的主要是讓大家學到一個koa2的基本流程,簡單實現koa2,再去讀源碼有一個清晰的思路。實際源碼中還有很多優秀的值得我們學習的點,接下來再列舉一個我覺得它很優秀的點——錯誤處理,大家可在原有基礎上繼續實現,也可以去讀源碼繼續看!加油加油
源碼中 koa 繼承自 Emiiter,為了處理可能在任意時間拋出的異常所以訂閱了 error 事件。error 處理有兩個層面,一個是 app 層面全局的(主要負責 log),另一個是一次響應過程中的 error 處理(主要決定響應的結果),koa 有一個默認 app-level 的 onerror 事件,用來輸出錯誤日志。
// 在調用洋蔥模型函數后面,koa 會掛載一個默認的錯誤處理【運行時確定異常處理】 if (!this.listenerCount("error")) this.on("error", this.onerror);
onerror(err) { if (!(err instanceof Error)) throw new TypeError(util.format("non-error thrown: %j", err)); if (404 == err.status || err.expose) return; if (this.silent) return; const msg = err.stack || err.toString(); console.error(); console.error(msg.replace(/^/gm, " ")); console.error(); }
通過 Emiiter 實現了錯誤打印,Emiiter 采用了發布訂閱的設計模式,如果有對 Emiiter 有不太清楚的小伙伴可以看我這篇文章
[源碼解讀]一文徹底搞懂Events模塊
。
總結
本文注重思想,精簡版本,代碼與實現都很簡單。封裝,遞歸,設計模式都說了一丟丟,希望也能對你有一丟丟的提升和讓你去看一下koa2源碼的想法,下篇文章見。
▼ 原創系列推薦▼
TypeScript真香系列——接口篇
消息隊列助你成為高薪 Node.js 工程師
深入理解Node.js 進程與線程(8000長文徹底搞懂)
[源碼解讀]一文徹底搞懂Events模塊
Node.js 高級進階之 fs 文件模塊學習
Node進階-探究不在V8堆內存中存儲的Buffer對象
說Node.js做后端開發,stream有必要了解下
點在看,分享給身邊的開發