從零開始手寫Koa2框架

01、介紹

  • Koa -- 基於 Node.js 平臺的下一代 web 開發框架
  • Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成爲 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。
  • 與其對應的 Express 來比,Koa 更加小巧、精壯,本文將帶大家從零開始實現 Koa 的源碼,從根源上解決大家對 Koa 的困惑
本文 Koa 版本爲 2.7.0, 版本不一樣源碼可能會有變動

02、源碼目錄介紹

  • Koa 源碼目錄截圖
    源碼目錄
  • 通過源碼目錄可以知道,Koa主要分爲4個部分,分別是:

    • application: Koa 最主要的模塊, 對應 app 應用對象
    • context: 對應 ctx 對象
    • request: 對應 Koa 中請求對象
    • response: 對應 Koa 中響應對象
  • 這4個文件就是 Koa 的全部內容了,其中 application 又是其中最核心的文件。我們將會從此文件入手,一步步實現 Koa 框架

03、實現一個基本服務器

  • 代碼目錄
  • my-application

    const {createServer} = require('http');
    
    module.exports = class Application {
      constructor() {
        // 初始化中間件數組, 所有中間件函數都會添加到當前數組中
        this.middleware = [];
      }
      // 使用中間件方法
      use(fn) {
        // 將所有中間件函數添加到中間件數組中
        this.middleware.push(fn);
      }
      // 監聽端口號方法
      listen(...args) {
        // 使用nodejs的http模塊監聽端口號
        const server = createServer((req, res) => {
          /*
            處理請求的回調函數,在這裏執行了所有中間件函數
            req 是 node 原生的 request 對象
            res 是 node 原生的 response 對象
          */
          this.middleware.forEach((fn) => fn(req, res));
        })
        server.listen(...args);
      }
    }
  • index.js

    // 引入自定義模塊
    const MyKoa = require('./js/my-application');
    // 創建實例對象
    const app = new MyKoa();
    // 使用中間件
    app.use((req, res) => {
      console.log('中間件函數執行了~~~111');
    })
    app.use((req, res) => {
      console.log('中間件函數執行了~~~222');
      res.end('hello myKoa');
    })
    // 監聽端口號
    app.listen(3000, err => {
      if (!err) console.log('服務器啓動成功了');
      else console.log(err);
    })
  • 運行入口文件 index.js 後,通過瀏覽器輸入網址訪問 http://localhost:3000/ , 就可以看到結果了~~
  • 神奇吧!一個最簡單的服務器模型就搭建完了。當然我們這個極簡服務器還存在很多問題,接下來讓我們一一解決

04、實現中間件函數的 next 方法

  • 提取createServer的回調函數,封裝成一個callback方法(可複用)

    // 監聽端口號方法
    listen(...args) {
      // 使用nodejs的http模塊監聽端口號
      const server = createServer(this.callback());
      server.listen(...args);
    }
    callback() {
      const handleRequest = (req, res) => {
        this.middleware.forEach((fn) => fn(req, res));
      }
      return handleRequest;
    }
  • 封裝compose函數實現next方法

    // 負責執行中間件函數的函數
    function compose(middleware) {
      // compose方法返回值是一個函數,這個函數返回值是一個promise對象
      // 當前函數就是調度
      return (req, res) => {
        // 默認調用一次,爲了執行第一個中間件函數
        return dispatch(0);
        function dispatch(i) {
          // 提取中間件數組的函數fn
          let fn = middleware[i];
          // 如果最後一箇中間件也調用了next方法,直接返回一個成功狀態的promise對象
          if (!fn) return Promise.resolve();
          /*
            dispatch.bind(null, i + 1)) 作爲中間件函數調用的第三個參數,其實就是對應的next
              舉個栗子:如果 i = 0  那麼 dispatch.bind(null, 1))  
                --> 也就是如果調用了next方法 實際上就是執行 dispatch(1) 
                  --> 它利用遞歸重新進來取出下一個中間件函數接着執行
            fn(req, res, dispatch.bind(null, i + 1))
              --> 這也是爲什麼中間件函數能有三個參數,在調用時我們傳進來了
          */
          return Promise.resolve(fn(req, res, dispatch.bind(null, i + 1)));
        }
      }
    }
  • 使用compose函數

    callback () {
      // 執行compose方法返回一個函數
      const fn = compose(this.middleware);
      
      const handleRequest = (req, res) => {
        // 調用該函數,返回值爲promise對象
        // then方法觸發了, 說明所有中間件函數都被調用完成
        fn(req, res).then(() => {
          // 在這裏就是所有處理的函數的最後階段,可以允許返回響應了~
        });
      }
      
      return handleRequest;
    }
  • 修改入口文件 index.js 代碼

    // 引入自定義模塊
    const MyKoa = require('./js/my-application');
    // 創建實例對象
    const app = new MyKoa();
    // 使用中間件
    app.use((req, res, next) => {
      console.log('中間件函數執行了~~~111');
      // 調用next方法,就是調用堆棧中下一個中間件函數
      next();
    })
    app.use((req, res, next) => {
      console.log('中間件函數執行了~~~222');
      res.end('hello myKoa');
      // 最後的next方法沒發調用下一個中間件函數,直接返回Promise.resolve()
      next();
    })
    // 監聽端口號
    app.listen(3000, err => {
      if (!err) console.log('服務器啓動成功了');
      else console.log(err);
    })
  • 此時我們實現了next方法,最核心的就是compose函數,極簡的代碼實現了功能,不可思議!

05、處理返回響應

  • 定義返回響應函數respond

    function respond(req, res) {
      // 獲取設置的body數據
      let body = res.body;
      
      if (typeof body === 'object') {
        // 如果是對象,轉化成json數據返回
        body = JSON.stringify(body);
        res.end(body);
      } else {
        // 默認其他數據直接返回
        res.end(body);
      }
    }
  • callback中調用

    callback() {
      const fn = compose(this.middleware);
      
      const handleRequest = (req, res) => {
        // 當中間件函數全部執行完畢時,會觸發then方法,從而執行respond方法返回響應
        const handleResponse = () => respond(req, res);
        fn(req, res).then(handleResponse);
      }
      
      return handleRequest;
    }
  • 修改入口文件 index.js 代碼

    // 引入自定義模塊
    const MyKoa = require('./js/my-application');
    // 創建實例對象
    const app = new MyKoa();
    // 使用中間件
    app.use((req, res, next) => {
      console.log('中間件函數執行了~~~111');
      next();
    })
    app.use((req, res, next) => {
      console.log('中間件函數執行了~~~222');
      // 設置響應內容,由框架負責返回響應~
      res.body = 'hello myKoa';
    })
    // 監聽端口號
    app.listen(3000, err => {
      if (!err) console.log('服務器啓動成功了');
      else console.log(err);
    })
  • 此時我們就能根據不同響應內容做出處理了~當然還是比較簡單的,可以接着去擴展~

06、定義 Request 模塊

// 此模塊需要npm下載
const parse = require('parseurl');
const qs = require('querystring');

module.exports = {
  /**
   * 獲取請求頭信息
   */
  get headers() {
    return this.req.headers;
  },
  /**
   * 設置請求頭信息
   */
  set headers(val) {
    this.req.headers = val;
  },
  /**
   * 獲取查詢字符串
   */
  get query() {
    // 解析查詢字符串參數 --> key1=value1&key2=value2
    const querystring = parse(this.req).query;
    // 將其解析爲對象返回 --> {key1: value1, key2: value2}
    return qs.parse(querystring);
  }
}

07、定義 Response 模塊

module.exports = {
  /**
   * 設置響應頭的信息
   */
  set(key, value) {
    this.res.setHeader(key, value);
  },
  /**
   * 獲取響應狀態碼
   */
  get status() {
    return this.res.statusCode;
  },
  /**
   * 設置響應狀態碼
   */
  set status(code) {
    this.res.statusCode = code;
  },
  /**
   * 獲取響應體信息
   */
  get body() {
    return this._body;
  },
  /**
   * 設置響應體信息
   */
  set body(val) {
    // 設置響應體內容
    this._body = val;
    // 設置響應狀態碼
    this.status = 200;
    // json
    if (typeof val === 'object') {
      this.set('Content-Type', 'application/json');
    }
  },
}

08、定義 Context 模塊

// 此模塊需要npm下載
const delegate = require('delegates');

const proto = module.exports = {};

// 將response對象上的屬性/方法克隆到proto上
delegate(proto, 'response')
  .method('set')    // 克隆普通方法
  .access('status') // 克隆帶有get和set描述符的方法
  .access('body')  

// 將request對象上的屬性/方法克隆到proto上
delegate(proto, 'request')
  .access('query')
  .getter('headers')  // 克隆帶有get描述符的方法

09、揭祕 delegates 模塊

module.exports = Delegator;

/**
 * 初始化一個 delegator.
 */
function Delegator(proto, target) {
  // this必須指向Delegator的實例對象
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  // 需要克隆的對象
  this.proto = proto;
  // 被克隆的目標對象
  this.target = target;
  // 所有普通方法的數組
  this.methods = [];
  // 所有帶有get描述符的方法數組
  this.getters = [];
  // 所有帶有set描述符的方法數組
  this.setters = [];
}

/**
 * 克隆普通方法
 */
Delegator.prototype.method = function(name){
  // 需要克隆的對象
  var proto = this.proto;
  // 被克隆的目標對象
  var target = this.target;
  // 方法添加到method數組中
  this.methods.push(name);
  // 給proto添加克隆的屬性
  proto[name] = function(){
    /*
      this指向proto, 也就是ctx
        舉個栗子:ctx.response.set.apply(ctx.response, arguments)
        arguments對應實參列表,剛好與apply方法傳參一致
        執行ctx.set('key', 'value') 實際上相當於執行 response.set('key', 'value')
    */
    return this[target][name].apply(this[target], arguments);
  };
  // 方便鏈式調用
  return this;
};

/**
 * 克隆帶有get和set描述符的方法.
 */
Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};

/**
 * 克隆帶有get描述符的方法.
 */
Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);
  // 方法可以爲一個已經存在的對象設置get描述符屬性
  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};

/**
 * 克隆帶有set描述符的方法.
 */
Delegator.prototype.setter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.setters.push(name);
  // 方法可以爲一個已經存在的對象設置set描述符屬性
  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });

  return this;
};

10、使用 ctx 取代 req 和 res

  • 修改 my-application

    const {createServer} = require('http');
    const context = require('./my-context');
    const request = require('./my-request');
    const response = require('./my-response');
    
    module.exports = class Application {
      constructor() {
        this.middleware = [];
        // Object.create(target) 以target對象爲原型, 創建新對象, 新對象原型有target對象的屬性和方法
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
      }
      
      use(fn) {
        this.middleware.push(fn);
      }
        
      listen(...args) {
        // 使用nodejs的http模塊監聽端口號
        const server = createServer(this.callback());
        server.listen(...args);
      }
      
      callback() {
        const fn = compose(this.middleware);
        
        const handleRequest = (req, res) => {
          // 創建context
          const ctx = this.createContext(req, res);
          const handleResponse = () => respond(ctx);
          fn(ctx).then(handleResponse);
        }
        
        return handleRequest;
      }
      
      // 創建context 上下文對象的方法
      createContext(req, res) {
        /*
          凡是req/res,就是node原生對象
          凡是request/response,就是自定義對象
          這是實現互相掛載引用,從而在任意對象上都能獲取其他對象的方法
         */
        const context = Object.create(this.context);
        const request = context.request = Object.create(this.request);
        const response = context.response = Object.create(this.response);
        context.app = request.app = response.app = this;
        context.req = request.req = response.req = req;
        context.res = request.res = response.res = res;
        request.ctx = response.ctx = context;
        request.response = response;
        response.request = request;
        
        return context;
      }
    }
    // 將原來使用req,res的地方改用ctx
    function compose(middleware) {
      return (ctx) => {
        return dispatch(0);
        function dispatch(i) {
          let fn = middleware[i];
          if (!fn) return Promise.resolve();
          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
        }
      }
    }
    
    function respond(ctx) {
      let body = ctx.body;
      const res = ctx.res;
      if (typeof body === 'object') {
        body = JSON.stringify(body);
        res.end(body);
      } else {
        res.end(body);
      }
    }
  • 修改入口文件 index.js 代碼

    // 引入自定義模塊
    const MyKoa = require('./js/my-application');
    // 創建實例對象
    const app = new MyKoa();
    // 使用中間件
    app.use((ctx, next) => {
      console.log('中間件函數執行了~~~111');
      next();
    })
    app.use((ctx, next) => {
      console.log('中間件函數執行了~~~222');
      // 獲取請求頭參數
      console.log(ctx.headers);
      // 獲取查詢字符串參數
      console.log(ctx.query);
      // 設置響應頭信息
      ctx.set('content-type', 'text/html;charset=utf-8');
      // 設置響應內容,由框架負責返回響應~
      ctx.body = '<h1>hello myKoa</h1>';
    })
    // 監聽端口號
    app.listen(3000, err => {
      if (!err) console.log('服務器啓動成功了');
      else console.log(err);
    })
到這裏已經寫完了 Koa 主要代碼,有一句古話 - 看萬遍代碼不如寫上一遍。 還等什麼,趕緊寫上一遍吧~
當你能夠寫出來,再去閱讀源碼,你會發現源碼如此簡單~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章