koa2的特點(diǎn)優(yōu)勢(shì)
什么是 koa2
- Nodejs官方api支持的都是callback形式的異步編程模型输虱。問(wèn)題:callback嵌套問(wèn)題
- 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)
- 傳統(tǒng)的 http 服務(wù)想使用
模塊化
不是很方便,我們不能在一個(gè)服務(wù)里面判斷所有的請(qǐng)求和一些內(nèi)容隧土。而 koa2 對(duì)模塊化提供了更好的幫助 - koa2 把 req提针,res 封裝到了
context
中,更簡(jiǎn)潔而且方便記憶 - 中間件機(jī)制曹傀,采用
洋蔥模型
,洋蔥模型流程記住一點(diǎn)(洋蔥是先從皮到心辐脖,然后從心到皮),通過(guò)洋蔥模型把代碼流程化
皆愉,讓流水線
更加清楚嗜价,如果不使用中間件,在createServer
一條線判斷所有邏輯確實(shí)不好幕庐。 - 看不到的優(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有必要了解下