從零實現TypeScript簡易版Koa

咱們知道Koa類庫主要有如下幾個重要特性:javascript

  • 支持洋蔥圈模型的中間件機制
  • 封裝request、response提供context對象,方便http操做
  • 異步函數、中間件的錯誤處理機制

本文將由淺到深帶領你們使用TS逐步完成一個實現了Koa核心功能的簡易框架前端

第一步:基礎Server運行

目標:完成基礎可行新的Koa Serverjava

  • 支持app.listen監聽端口啓動Server
  • 支持app.use添加類middleware處理函數

核心代碼以下:git

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");
});
複製代碼

第二步:洋蔥圈中間件機制實現

目標:接下來咱們要完善listen和use方法,實現洋蔥圈中間件模型github

以下面代碼所示,在這一步中咱們但願app.use可以支持添加多箇中間件,而且中間件是按照洋蔥圈(相似深度遞歸調用)的方式順序執行數組

app.use(async (req, res, next) => {
  console.log("middleware 1 start");
  // 這裏有兩個須要注意的點:
  // 一、next()函數必須且只能調用一次
  // 二、調用next函數時必須使用await
  // 具體緣由咱們會在下面代碼實現詳細講解
  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");
});
複製代碼

下面咱們來看一看具體怎麼實現這種洋蔥圈機制:app

class Koa {
  ...
  use(middlewareFn: middlewareFn) {
    // 一、調用use時,使用數組存貯全部的middleware
    this.middlewares.push(middlewareFn);
    return this;
  }
  listen(port: number, cb: noop) {
    // 二、 經過composeMiddleware將中間件數組轉換爲串行[洋蔥圈]調用的函數,在createServer中回調函數中調用
    // 因此真正的重點就是 composeMiddleware,若是作到的,咱們接下來看該函數的實現
    // BTW: 從這裏能夠看到 fn 是在listen函數被調用以後就生成了,這就意味着咱們不能在運行時動態的添加middleware
    const fn = composeMiddleware(this.middlewares);
    const server = http.createServer(async (req, res) => {
      await fn(req, res);
    });
    return server.listen(port, cb);
  }
}

// 三、洋蔥圈模型的核心:
// 入參:全部收集的中間件
// 返回:串行調用中間件數組的函數
function composeMiddleware(middlewares: middlewareFn[]) {
  return (req: IncomingMessage, res: ServerResponse) => {
    let start = -1;
    // dispatch:觸發第i箇中間件執行
    function dispatch(i: number) {
      // 剛開始可能不理解這裏爲何這麼判斷,能夠看完整個函數在來思考這個問題
      // 正常狀況下每次調用前 start < i,調用完next() 應該 start === i
      // 若是調用屢次next(),第二次及之後調用由於以前已完成start === i賦值,因此會致使 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];
      // 重點來了!!!
      // 取出第i箇中間件執行,並將dispatch(i+1)做爲next傳給各下一個中間件
      // 如今咱們在回顧以前提出的兩個問題:
      // 1. koa中間件中爲何必須且只能調用一次next函數
      // 能夠看到若是不調用next,下一個中間件就沒辦法觸發,形成假死狀態最終請求超時
      // 調用屢次next則會到時下一個中間件執行屢次
      // 2. next() 調用爲何須要加 await
      // 這也是洋蔥圈調用機制的核心,當執行到 await next(),會執行next()【調用下一個中間件】等待返回結果,在接着向下執行
      return middleware(req, res, () => {
        return dispatch(i + 1);
      });
    }
    return dispatch(0);
  };
}
複製代碼

第三步:Context提供

目標:封裝Context,提供request、response的便捷操做方式框架

// 一、 定義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) {
      // ..省略其餘代碼..
      // 二、全部的中間件接受context參數
      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) => {
      // 三、利用req、res建立context對象
      // 這裏須要注意:context是建立一個新的對象,而不是直接賦值給this.context
      // 由於context適合請求相關聯的,這裏也保證了每個請求都是一個新的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);
  }
  // 四、建立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;
  }
}
複製代碼

第四步:異步函數錯誤處理機制

目標:支持經過 app.on("error"),監聽錯誤事件處理異常koa

咱們回憶下在Koa中如何處理異常,代碼可能相似以下:異步

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

// koa統一錯誤處理:監聽error事件
app.on("error", (error, context) => {
  console.error(`請求${context.url}發生了錯誤`);
});
複製代碼

從上面的代碼能夠看到核心在於:

  • Koa實例app須要支持事件觸發、事件監聽能力
  • 須要咱們捕獲異步函數異常,並觸發error事件

下面咱們看具體代碼如何實現:

// 一、繼承EventEmitter,增長事件觸發、監聽能力
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);
      // 二、await調用fn,可使用try catch捕獲異常,觸發異常事件
      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");
        // 三、觸發error時提供context更多信息,方面日誌記錄,定位問題
        this.emit("error", error, context);
      }
    });
    return server.listen(port, cb);
  }
}
複製代碼

總結

至此咱們已經使用TypeScript完成簡版Koa類庫,支持了

  • 洋蔥圈中間件機制
  • Context封裝request、response
  • 異步異常錯誤處理機制

完整Demo代碼能夠參考koa2-reference

更多精彩文章,歡迎你們Star咱們的倉庫、關注咱們的掘金號,咱們每週都會推出幾篇高質量的大前端領域相關文章。

參考資料

相關文章
相關標籤/搜索