讀 koa2 源碼后的一些思考與實(shí)踐

koa2的特點(diǎn)優(yōu)勢(shì)

什么是 koa2

  1. Nodejs官方api支持的都是callback形式的異步編程模型输虱。問(wèn)題:callback嵌套問(wèn)題
  2. koa2 是由 Express原班人馬打造的构捡,是現(xiàn)在比較流行的基于Node.js平臺(tái)的web開(kāi)發(fā)框架,Koa 把 Express 中內(nèi)置的 router、view 等功能都移除了蒿讥,使得框架本身更輕量达椰,而且擴(kuò)展性很強(qiáng)。使用koa編寫(xiě)web應(yīng)用屎篱,可以免除重復(fù)繁瑣的回調(diào)函數(shù)服赎。

koa2 的優(yōu)點(diǎn)

優(yōu)點(diǎn)這個(gè)東西,我直接說(shuō)它多好交播,你可能又不開(kāi)心重虑,但是我們可以對(duì)比哦!這里我只說(shuō)它對(duì)比原生的 Node.js開(kāi)啟 http 服務(wù) 帶來(lái)了哪些優(yōu)點(diǎn)秦士!

  • 先看一下原生 Node.js 我開(kāi)啟一個(gè) http 服務(wù)
const http = require('http');

http.createServer((req,res)=>{
res.writeHead(200);
res.end('hi koala');
}).listen(3000);
  • 看一下使用 koa2 開(kāi)啟一個(gè)http 服務(wù)
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 中添加了一個(gè)判斷 /favicon.ico 的實(shí)現(xiàn) 通過(guò)以上兩段代碼缺厉,會(huì)發(fā)現(xiàn)下面幾個(gè)優(yōu)點(diǎn)

  1. 傳統(tǒng)的 http 服務(wù)想使用模塊化不是很方便,我們不能在一個(gè)服務(wù)里面判斷所有的請(qǐng)求和一些內(nèi)容隧土。而 koa2 對(duì)模塊化提供了更好的幫助
  2. koa2 把 req提针,res 封裝到了 context 中,更簡(jiǎn)潔而且方便記憶
  3. 中間件機(jī)制曹傀,采用洋蔥模型,洋蔥模型流程記住一點(diǎn)(洋蔥是先從皮到心辐脖,然后從心到皮),通過(guò)洋蔥模型把代碼流程化皆愉,讓流水線更加清楚嗜价,如果不使用中間件,在 createServer 一條線判斷所有邏輯確實(shí)不好幕庐。
  4. 看不到的優(yōu)點(diǎn)也很多久锥,error 錯(cuò)誤處理,res的封裝處理等翔脱。

自己實(shí)現(xiàn)一個(gè)koa2

在實(shí)現(xiàn)的過(guò)程中會(huì)我看看可以學(xué)到那些知識(shí)

listen 函數(shù)簡(jiǎn)單封裝

koa2 直接使用的時(shí)候奴拦,我們通過(guò) const app = new Koa();,koa 應(yīng)該是一個(gè)類,而且可以直接調(diào)用 listen 函數(shù)届吁,并且沒(méi)有暴漏出http 服務(wù)的創(chuàng)建错妖,說(shuō)明在listen函數(shù)中可能創(chuàng)建了服務(wù)绿鸣。到此簡(jiǎn)單代碼實(shí)現(xiàn)應(yīng)該是這樣的:

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;

實(shí)現(xiàn) context 的封裝

實(shí)現(xiàn)了簡(jiǎn)單 listen 后,會(huì)發(fā)現(xiàn)回調(diào)函數(shù)返回的還是 req 和 res 暂氯,要是將二者封裝到 context 一次返回就更好了潮模!我們繼續(xù)

 const ctx = this.createContext(req,res);

看一下 createContext 的具體實(shí)現(xiàn)

const request = require('./lib/request');
const response = require('./lib/response');
const context = require('./lib/context');

createContext(req,res){

// 創(chuàng)建一個(gè)新對(duì)象,繼承導(dǎo)入的context
const ctx = Object.create(context);
ctx.request = Object.create(request);
ctx.response = Object.create(response);
// 這里的兩等于判斷痴施,讓使用者既可以直接使用ctx擎厢,也可以使用原生的內(nèi)容
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;
}
}

這里在寫(xiě) context.js 時(shí)候,用到了set 與 get 函數(shù)辣吃,get 語(yǔ)句作為函數(shù)綁定在對(duì)象的屬性上,當(dāng)訪問(wèn)該屬性時(shí)調(diào)用該函數(shù)动遭。set 語(yǔ)法可以將一個(gè)函數(shù)綁定在當(dāng)前對(duì)象的指定屬性上,當(dāng)那個(gè)屬性被賦值時(shí)神得,你所綁定的函數(shù)就會(huì)被調(diào)用厘惦。

實(shí)現(xiàn)洋蔥模型

compose 另一個(gè)應(yīng)用場(chǎng)景

說(shuō)洋蔥模型之前先看一個(gè)函數(shù)式編程內(nèi)容:compose 函數(shù)前端用過(guò) redux 的同學(xué)肯定都很熟悉。redux 通過(guò)compose來(lái)處理 中間件 哩簿。原理是 借助數(shù)組的 reduce 對(duì)數(shù)組的參數(shù)進(jìn)行迭代

// redux 中的 compose 函數(shù)

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)))
}

洋蔥模型實(shí)現(xiàn)

再看文章開(kāi)頭 koa2 創(chuàng)建 http 服務(wù)函數(shù)宵蕉,會(huì)發(fā)現(xiàn)多次調(diào)用 use 函數(shù),其實(shí)這就是洋蔥模型的應(yīng)用节榜。

洋蔥是由很多層組成的,你可以把每個(gè)中間件看作洋蔥里的一層,根據(jù)app.use的調(diào)用順序中間件由外層到里層組成了整個(gè)洋蔥,整個(gè)中間件執(zhí)行過(guò)程相當(dāng)于由外到內(nèi)再到外地穿透整個(gè)洋蔥

引用一張著名的洋蔥模型圖:

每次執(zhí)行 use 函數(shù)羡玛,我們實(shí)際是往一個(gè)函數(shù)數(shù)組中添加了一個(gè)函數(shù),然后再次通過(guò)一個(gè) compose 函數(shù)宗苍,處理添加進(jìn)來(lái)函數(shù)的執(zhí)行順序稼稿,也就是這個(gè) compose 函數(shù)實(shí)現(xiàn)了洋蔥模型機(jī)制。

具體代碼實(shí)現(xiàn)如下:

// 其中包含一個(gè)遞歸
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)
})
)
}
}
}

首先執(zhí)行一次 dispatch(0) 也就是默認(rèn)返回第一個(gè) app.use 傳入的函數(shù) 使用 Promise 函數(shù)封裝返回讳窟,其中第一個(gè)參數(shù)是我們常用的 ctx渺杉,

第二個(gè)參數(shù)就是 next 參數(shù),next 每次執(zhí)行之后都會(huì)等于下一個(gè)中間件函數(shù)挪钓,如果下一個(gè)中間件函數(shù)不為真則返回一個(gè)成功的 Promise。因此我們每次調(diào)用 next() 就是在執(zhí)行下一個(gè)中間件函數(shù)耳舅。

來(lái)試試我們自己實(shí)現(xiàn)的koa2

使用一下我們自己的 koa2 吧碌上,用它做一道常考洋蔥模型面試題浦徊,我想文章如果懂了馏予,輸出結(jié)果應(yīng)該不會(huì)錯(cuò)了,自己試一下盔性!

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

補(bǔ)充與說(shuō)明

本文目的主要是讓大家學(xué)到一個(gè)koa2的基本流程冕香,簡(jiǎn)單實(shí)現(xiàn)koa2蛹尝,再去讀源碼有一個(gè)清晰的思路后豫。實(shí)際源碼中還有很多優(yōu)秀的值得我們學(xué)習(xí)的點(diǎn),接下來(lái)再列舉一個(gè)我覺(jué)得它很優(yōu)秀的點(diǎn)——錯(cuò)誤處理突那,大家可在原有基礎(chǔ)上繼續(xù)實(shí)現(xiàn)挫酿,也可以去讀源碼繼續(xù)看!加油加油

源碼中 koa 繼承自 Emiiter愕难,為了處理可能在任意時(shí)間拋出的異常所以訂閱了 error 事件早龟。error 處理有兩個(gè)層面,一個(gè)是 app 層面全局的(主要負(fù)責(zé) log)猫缭,另一個(gè)是一次響應(yīng)過(guò)程中的 error 處理(主要決定響應(yīng)的結(jié)果)葱弟,koa 有一個(gè)默認(rèn) app-level 的 onerror 事件,用來(lái)輸出錯(cuò)誤日志猜丹。

 // 在調(diào)用洋蔥模型函數(shù)后面芝加,koa 會(huì)掛載一個(gè)默認(rèn)的錯(cuò)誤處理【運(yùn)行時(shí)確定異常處理】
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();
}

通過(guò) Emiiter 實(shí)現(xiàn)了錯(cuò)誤打印,Emiiter 采用了發(fā)布訂閱的設(shè)計(jì)模式居触,如果有對(duì) Emiiter 有不太清楚的小伙伴可以看我這篇文章

[源碼解讀]一文徹底搞懂Events模塊

妖混。

總結(jié)

本文注重思想,精簡(jiǎn)版本轮洋,代碼與實(shí)現(xiàn)都很簡(jiǎn)單制市。封裝,遞歸弊予,設(shè)計(jì)模式都說(shuō)了一丟丟祥楣,希望也能對(duì)你有一丟丟的提升和讓你去看一下koa2源碼的想法,下篇文章見(jiàn)汉柒。

? 原創(chuàng)系列推薦 ▼

TypeScript真香系列——接口篇

消息隊(duì)列助你成為高薪 Node.js 工程師


深入理解Node.js 進(jìn)程與線程(8000長(zhǎng)文徹底搞懂)

[源碼解讀]一文徹底搞懂Events模塊


Node.js 高級(jí)進(jìn)階之 fs 文件模塊學(xué)習(xí)


Node進(jìn)階-探究不在V8堆內(nèi)存中存儲(chǔ)的Buffer對(duì)象


說(shuō)Node.js做后端開(kāi)發(fā)误褪,stream有必要了解下


點(diǎn)在看,分享給身邊的開(kāi)發(fā)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末碾褂,一起剝皮案震驚了整個(gè)濱河市兽间,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌正塌,老刑警劉巖嘀略,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乓诽,居然都是意外死亡帜羊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)鸠天,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)讼育,“玉大人,你說(shuō)我怎么就攤上這事∧潭危” “怎么了饥瓷?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)忧饭。 經(jīng)常有香客問(wèn)我扛伍,道長(zhǎng),這世上最難降的妖魔是什么词裤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任刺洒,我火速辦了婚禮,結(jié)果婚禮上吼砂,老公的妹妹穿的比我還像新娘逆航。我一直安慰自己,他們只是感情好渔肩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布因俐。 她就那樣靜靜地躺著,像睡著了一般周偎。 火紅的嫁衣襯著肌膚如雪抹剩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天蓉坎,我揣著相機(jī)與錄音澳眷,去河邊找鬼。 笑死蛉艾,一個(gè)胖子當(dāng)著我的面吹牛钳踊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播勿侯,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拓瞪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了助琐?” 一聲冷哼從身側(cè)響起祭埂,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兵钮,沒(méi)想到半個(gè)月后沟堡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡矢空,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了禀横。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屁药。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖柏锄,靈堂內(nèi)的尸體忽然破棺而出酿箭,到底是詐尸還是另有隱情复亏,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布缭嫡,位于F島的核電站缔御,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏妇蛀。R本人自食惡果不足惜耕突,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望评架。 院中可真熱鬧眷茁,春花似錦、人聲如沸纵诞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浙芙。三九已至登刺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嗡呼,已是汗流浹背纸俭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晤锥,地道東北人掉蔬。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像矾瘾,于是被迫代替她去往敵國(guó)和親女轿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

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

  • 閱讀前所需知識(shí) 擁有Node.js語(yǔ)言基礎(chǔ) 了解http模塊 有Koa框架使用經(jīng)驗(yàn) 首先看一下官方的HelloWo...
    汪汪收房租閱讀 263評(píng)論 0 0
  • 最近在學(xué)習(xí)koa2,但是自己陷入了瓶頸期壕翩。就是不知道學(xué)什么好,對(duì)未來(lái)有點(diǎn)迷茫蛉迹。還好最近看到了知乎上的狼叔的文章 感...
    Djknight閱讀 979評(píng)論 2 5
  • 前言 Koa 是運(yùn)行在 Node.js 中的 web 服務(wù)框架,小而美放妈。 Koa2 是 Koa 框架的最新版本北救,K...
    let_Scott閱讀 5,775評(píng)論 2 28
  • Koa源碼解析 整體架構(gòu) 核心文件只有4個(gè),在lib文件夾下: application.js koa框架的入口...
    Ethan_lcm閱讀 2,433評(píng)論 0 1
  • Q1:什么是中間件 中間件(Middleware)芜抒,也叫中介層珍策,是提供系統(tǒng)軟件和應(yīng)用軟件之間連接的軟件,便于軟件各...
    BubbleM閱讀 554評(píng)論 0 0