讀 Koa2 源碼後的一些思考與實現(面試必備)

koa2的特色優點

什麼是 koa2

  1. Nodejs官方api支持的都是callback形式的異步編程模型。問題:callback嵌套問題
  2. koa2 是由 Express原班人馬打造的,是如今比較流行的基於Node.js平臺的web開發框架,Koa 把 Express 中內置的 router、view 等功能都移除了,使得框架自己更輕量,並且擴展性很強。使用koa編寫web應用,能夠免除重複繁瑣的回調函數。

做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…javascript

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 的實現 經過以上兩段代碼,會發現下面三個優勢java

  1. 傳統的 http 服務想使用模塊化不是很方便,咱們不能在一個服務裏面判斷全部的請求和一些內容。而 koa2 對模塊化提供了更好的幫助
  2. koa2 把 req,res 封裝到了 context 中,更簡潔並且方便記憶
  3. 中間件機制,採用洋蔥模型,洋蔥模型流程記住一點(洋蔥是先從皮到心,而後從心到皮),經過洋蔥模型把代碼流程化,讓流水線更加清楚,若是不使用中間件,在 createServer 一條線判斷全部邏輯確實很差。
  4. 看不到的優勢也不少,error 錯誤處理,res的封裝處理等。

本身實現一個koa2

在實現的過程當中會我看看能夠學到那些知識git

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 一次返回就更好了!咱們繼續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 另外一個應用場景

說洋蔥模型以前先看一個函數式編程內容: 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 源碼的想法,下篇文章見。

Node系列原創文章

深刻理解Node.js 中的進程與線程

想學Node.js,stream先有必要搞清楚

require時,exports和module.exports的區別你真的懂嗎

源碼解讀一文完全搞懂Events模塊

Node.js 高級進階之 fs 文件模塊學習

關注我

  • 歡迎加我微信【coder_qi】,拉你進技術羣,長期交流學習...
  • 歡迎關注「程序員成長指北」,一個用心幫助你成長的公衆號...

相關文章
相關標籤/搜索