從零實(shí)現(xiàn)TypeScript版Koa

這篇文章會(huì)講些什么?

  • 如何從零開始完成一個(gè)涵蓋Koa核心功能的Node.js類庫
  • 從代碼層面解釋Koa一些代碼寫法的原因:如中間件為什么必須調(diào)用next函數(shù)、ctx是怎么來的和一個(gè)請求是什么關(guān)系

我們知道Koa類庫主要有以下幾個(gè)重要特性:

  • 支持洋蔥圈模型的中間件機(jī)制
  • 封裝request、response提供context對象,方便http操作
  • 異步函數(shù)、中間件的錯(cuò)誤處理機(jī)制

第一步:基礎(chǔ)Server運(yùn)行

目標(biāo):完成基礎(chǔ)可行新的Koa Server

  • 支持app.listen監(jiān)聽端口啟動(dòng)Server
  • 支持app.use添加類middleware處理函數(shù)

核心代碼如下:

class Koa {
  private middleware: middlewareFn = () => {};
  constructor() {}
  listen(port: number, cb: noop) {
    const server = http.createServer((req, res) => {
      this.middleware(req, res);
    });
    return server.listen(port, cb);
  }
  use(middlewareFn: middlewareFn) {
    this.middleware = middlewareFn;
    return this;
  }
}

const app = new Koa();
app.use((req, res) => {
  res.writeHead(200);
  res.end("A request come in");
});
app.listen(3000, () => {
  console.log("Server listen on port 3000");
});

第二步:洋蔥圈中間件機(jī)制實(shí)現(xiàn)

目標(biāo):接下來我們要完善listen和use方法,實(shí)現(xiàn)洋蔥圈中間件模型

如下面代碼所示,在這一步中我們希望app.use能夠支持添加多個(gè)中間件,并且中間件是按照洋蔥圈(類似深度遞歸調(diào)用)的方式順序執(zhí)行

app.use(async (req, res, next) => {
  console.log("middleware 1 start");
  // 具體原因我們會(huì)在下面代碼實(shí)現(xiàn)詳細(xì)講解
  await next();
  console.log("middleware 1 end");
});
app.use(async (req, res, next) => {
  console.log("middleware 2 start");
  await next();
  console.log("middleware 2 end");
});
app.use(async (req, res, next) => {
  res.writeHead(200);
  res.end("An request come in");
  await next();
});
app.listen(3000, () => {
  console.log("Server listen on port 3000");
});

上述Demo有三個(gè)需要我們注意的點(diǎn):

  • 在中間件中next()函數(shù)必須且只能調(diào)用一次
  • 調(diào)用next函數(shù)時(shí)必須使用await

我們會(huì)在接下來的代碼中逐個(gè)分析這些使用方法的原因,下面我們來看一看具體怎么實(shí)現(xiàn)這種洋蔥圈機(jī)制:

class Koa {
  ...
  use(middlewareFn: middlewareFn) {
    // 1、調(diào)用use時(shí),使用數(shù)組存貯所有的middleware
    this.middlewares.push(middlewareFn);
    return this;
  }
  listen(port: number, cb: noop) {
    // 2、 通過composeMiddleware將中間件數(shù)組轉(zhuǎn)換為串行[洋蔥圈]調(diào)用的函數(shù),在createServer中回調(diào)函數(shù)中調(diào)用
    // 所以真正的重點(diǎn)就是 composeMiddleware,如果做到的,我們接下來看該函數(shù)的實(shí)現(xiàn)
    // BTW: 從這里可以看到 fn 是在listen函數(shù)被調(diào)用之后就生成了,這就意味著我們不能在運(yùn)行時(shí)動(dòng)態(tài)的添加middleware
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      await fn(req, res);
    });
    return server.listen(port, cb);
  }
}

// 3、洋蔥圈模型的核心:
// 入?yún)ⅲ核惺占闹虚g件
// 返回:串行調(diào)用中間件數(shù)組的函數(shù)
function composeMiddleware(middlewares: middlewareFn[]) {
  return (req: IncomingMessage, res: ServerResponse) => {
    let start = -1;
    // dispatch:觸發(fā)第i個(gè)中間件執(zhí)行
    function dispatch(i: number) {
      // 剛開始可能不理解這里為什么這么判斷,可以看完整個(gè)函數(shù)在來思考這個(gè)問題
      // 正常情況下每次調(diào)用前 start < i,調(diào)用完next() 應(yīng)該 start === i
      // 如果調(diào)用多次next(),第二次及以后調(diào)用因?yàn)橹耙淹瓿蓅tart === i賦值,所以會(huì)導(dǎo)致 start >= i
      if (i <= start) {
        return Promise.reject(new Error("next() call more than once!"));
      }
      if (i >= middlewares.length) {
        return Promise.resolve();
      }
      start = i;
      const middleware = middlewares[i];
      // 重點(diǎn)來了!??!
      // 取出第i個(gè)中間件執(zhí)行,并將dispatch(i+1)作為next函數(shù)傳給各下一個(gè)中間件
      return middleware(req, res, () => {
        return dispatch(i + 1);
      });
    }
    return dispatch(0);
  };
}

主要涉及到Promise幾個(gè)知識點(diǎn):

  • async 函數(shù)返回的是一個(gè)Promise對象【所以的中間件都會(huì)返回一個(gè)promise對象】
  • async 函數(shù)內(nèi)部遇到 await 調(diào)用時(shí)會(huì)暫停執(zhí)行await函數(shù),等待返回結(jié)果后繼續(xù)向下執(zhí)行
  • async 函數(shù)內(nèi)部發(fā)生錯(cuò)誤會(huì)導(dǎo)致返回的Promise變?yōu)閞eject狀態(tài)

現(xiàn)在我們在回顧之前提出的幾個(gè)問題:

  1. koa中間件中為什么必須且只能調(diào)用一次next函數(shù)

     可以看到如果不調(diào)用next,就不會(huì)觸發(fā)dispatch(i+1),下一個(gè)中間件就沒辦法觸發(fā),造成假死狀態(tài)最終請求超時(shí)
     
     調(diào)用多次next則會(huì)導(dǎo)致下一個(gè)中間件執(zhí)行多次
    
  2. next() 調(diào)用為什么需要加 await

     這也是洋蔥圈調(diào)用機(jī)制的核心,當(dāng)執(zhí)行到 await next(),會(huì)執(zhí)行next()【調(diào)用下一個(gè)中間件】等待返回結(jié)果,在接著向下執(zhí)行
    

第三步:Context提供

目標(biāo):封裝Context,提供request、response的便捷操作方式

// 1、 定義KoaRequest、KoaResponse、KoaContext
interface KoaContext {
  request?: KoaRequest;
  response?: KoaResponse;
  body: String | null;
}
const context: KoaContext = {
  get body() {
    return this.response!.body;
  },
  set body(body) {
    this.response!.body = body;
  }
};

function composeMiddleware(middlewares: middlewareFn[]) {
  return (context: KoaContext) => {
    let start = -1;
    function dispatch(i: number) {
      // ..省略其他代碼..
      // 2、所有的中間件接受context參數(shù)
      middleware(context, () => {
        return dispatch(i + 1);
      });
    }
    return dispatch(0);
  };
}

class Koa {
  private context: KoaContext = Object.create(context);
  listen(port: number, cb: noop) {
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      // 3、利用req、res創(chuàng)建context對象
      // 這里需要注意:context是創(chuàng)建一個(gè)新的對象,而不是直接賦值給this.context
      // 因?yàn)閏ontext適合請求相關(guān)聯(lián)的,這里也保證了每一個(gè)請求都是一個(gè)新的context對象
      const context = this.createContext(req, res);
      await fn(context);
      if (context.response && context.response.res) {
        context.response.res.writeHead(200);
        context.response.res.end(context.body);
      }
    });
    return server.listen(port, cb);
  }
  // 4、創(chuàng)建context對象
  createContext(req: IncomingMessage, res: ServerResponse): KoaContext {
    // 為什么要使用Object.create而不是直接賦值?
    // 原因同上需要保證每一次請求request、response、context都是全新的
    const request = Object.create(this.request);
    const response = Object.create(this.response);
    const context = Object.create(this.context);
    request.req = req;
    response.res = res;
    context.request = request;
    context.response = response;
    return context;
  }
}

第四步:異步函數(shù)錯(cuò)誤處理機(jī)制

目標(biāo):支持通過 app.on("error"),監(jiān)聽錯(cuò)誤事件處理異常

我們回憶下在Koa中如何處理異常,代碼可能類似如下:

app.use(async (context, next) => {
  console.log("middleware 2 start");
  // throw new Error("出錯(cuò)了");
  await next();
  console.log("middleware 2 end");
});

// koa統(tǒng)一錯(cuò)誤處理:監(jiān)聽error事件
app.on("error", (error, context) => {
  console.error(`請求${context.url}發(fā)生了錯(cuò)誤`);
});

從上面的代碼可以看到核心在于:

  • Koa實(shí)例app需要支持事件觸發(fā)、事件監(jiān)聽能力
  • 需要我們捕獲異步函數(shù)異常,并觸發(fā)error事件

下面我們看具體代碼如何實(shí)現(xiàn):

// 1、繼承EventEmitter,增加事件觸發(fā)、監(jiān)聽能力
class Koa extends EventEmitter {
  listen(port: number, cb: noop) {
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      const context = this.createContext(req, res);
      // 2、await調(diào)用fn,可以使用try catch捕獲異常,觸發(fā)異常事件
      try {
        await fn(context);
        if (context.response && context.response.res) {
          context.response.res.writeHead(200);
          context.response.res.end(context.body);
        }
      } catch (error) {
        console.error("Server Error");
        // 3、觸發(fā)error時(shí)提供context更多信息,方面日志記錄,定位問題
        this.emit("error", error, context);
      }
    });
    return server.listen(port, cb);
  }
}

總結(jié)

至此我們已經(jīng)使用TypeScript完成簡版Koa類庫,支持了

  • 洋蔥圈中間件機(jī)制
  • Context封裝request、response
  • 異步異常錯(cuò)誤處理機(jī)制

完整Demo代碼可以參考koa2-reference

更多精彩文章,歡迎大家Star我們的倉庫,我們每周都會(huì)推出幾篇高質(zhì)量的大前端領(lǐng)域相關(guān)文章。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本節(jié)將結(jié)合例子和源碼對koa2的中間件機(jī)制做一介紹。 什么是中間件? 中間件的本質(zhì)就是一種在特定場景下使用的函數(shù),...
    空無一碼閱讀 1,534評論 0 2
  • 參考資料 https://chenshenhai.github.io/koa2-note/note/static/...
    JunChow520閱讀 10,645評論 1 8
  • 弄懂js異步 講異步之前,我們必須掌握一個(gè)基礎(chǔ)知識-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,884評論 0 5
  • 看到標(biāo)題,也許您會(huì)覺得奇怪,redux跟Koa以及Express并不是同一類別的框架,干嘛要拿來做類比。盡管,例如...
    Perkin_閱讀 1,809評論 0 4
  • 陸陸續(xù)續(xù)用了koa和co也算差不多用了大半年了,大部分的場景都是在服務(wù)端使用koa來作為restful服務(wù)器用,使...
    Sunil閱讀 1,680評論 0 3

友情鏈接更多精彩內(nèi)容