做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…javascript
優勢這個東西,我直接說它多好,你可能又不開心,可是咱們能夠對比哦!這裏我只說它對比原生的 Node.js開啓 http 服務 帶來了哪些優勢!前端
const http = require('http');
http.createServer((req,res)=>{
res.writeHead(200);
res.end('hi koala');
}).listen(3000);
複製代碼
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 的實現 經過以上兩段代碼,會發現下面三個優勢java
模塊化
不是很方便,咱們不能在一個服務裏面判斷全部的請求和一些內容。而 koa2 對模塊化提供了更好的幫助context
中,更簡潔並且方便記憶洋蔥模型
,洋蔥模型流程記住一點(洋蔥是先從皮到心,而後從心到皮),經過洋蔥模型把代碼流程化
,讓流水線
更加清楚,若是不使用中間件,在 createServer
一條線判斷全部邏輯確實很差。在實現的過程當中會我看看能夠學到那些知識git
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;
複製代碼
實現了簡單 listen 後,會發現回調函數返回的仍是 req 和 res ,要是將兩者封裝到 context 一次返回就更好了!咱們繼續github
const ctx = this.createContext(req,res);
複製代碼
看一下 createContext 的具體實現web
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 函數前端用過 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 吧,用它作一道常考洋蔥模型面試題,我想文章若是懂了,輸出結果應該不會錯了,本身試一下!
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 源碼的想法,下篇文章見。
require時,exports和module.exports的區別你真的懂嗎