koa2源碼解讀及實現一個簡單的koa2框架


閱讀目錄

koa2源碼文件如下結構:
|-- lib
|    |--- application.js
|    |--- context.js
|    |--- request.js
|    |--- response.js
|__ package.json

application.js 是Koa2的入口文件,它封裝了 context, request, response, 及 中間件處理的流程, 及 它向外導出了class的實列,並且它繼承了Event, 因此該框架支持事件監聽和觸發的能力,比如代碼: module.exports = class Application extends Emitter {}.

context.js 是處理應用的上下文ctx。它封裝了 request.js 和 response.js 的方法。
request.js 它封裝了處理http的請求。
response.js 它封裝了處理http響應。

因此實現koa2框架需要封裝和實現如下四個模塊:

1. 封裝node http server. 創建koa類構造函數。
2. 構造request、response、及 context 對象。
3. 中間件機制的實現。
4. 錯誤捕獲和錯誤處理。

一:封裝node http server. 創建koa類構造函數

 首先,如果我們使用node的原生模塊實現一個簡單的服務器,並且打印 hello world,代碼一般是如下所示:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end('hello world....');
});

server.listen(3000, () => {
  console.log('listening on 3000');
});

因此實現koa的第一步是,我們需要對該原生模塊進行封裝一下,我們首先要創建application.js實現一個Application對象。

基本代碼封裝成如下(假如我們把代碼放到 application.js里面):

const Emitter = require('events');
const http = require('http');

class Application extends Emitter {
  /* 構造函數 */
  constructor() {
    super();
    this.callbackFunc = null;
  }
  // 開啟 http server 並且傳入參數 callback
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  use(fn) {
    this.callbackFunc = fn;
  }
  callback() {
    return (req, res) => {
      this.callbackFunc(req, res);
    }
  }
}

module.exports = Application;

然后我們在該目錄下新建一個 test.js 文件,使用如下代碼進行初始化如下:

const testKoa = require('./application');
const app = new testKoa();

app.use((req, res) => {
  res.writeHead(200);
  res.end('hello world....');
});

app.listen(3000, () => {
  console.log('listening on 3000');
});

如上基本代碼我們可以看到,在application.js 我們簡單的封裝了一個 http server,使用app.use注冊回調函數,app.listen監聽server,並傳入回調函數。

但是如上代碼有個缺點,app.use 傳入的回調函數參數還是req,res, 也就是node原生的request和response對象,使用該對象還是不夠方便,它不符合框架的設計的易用性,我們需要封裝成如下的樣子:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log(11111);
  await next();
  console.log(22222);
});
app.listen(3000, () => {
  console.log('listening on 3000');
});

基於以上的原因,我們需要構造 request, response, 及 context對象了。

二:構造request、response、及 context 對象。

2.1 request.js

該模塊的作用是對原生的http模塊的request對象進行封裝,對request對象的某些屬性或方法通過重寫 getter/setter函數進行代理。

因此我們需要在我們項目中根目錄下新建一個request.js, 該文件只有獲取和設置url的方法,最后導出該文件,代碼如下:

const request = {
  get url() {
    return this.req.url;
  },
  set url(val) {
    this.req.url = val;
  }
};

module.exports = request;

如需要理解get/set對對象的監聽可以看我這篇文章

如上代碼很簡單,導出一個對象,該文件中包含了獲取和設置url的方法,代碼中this.req是node原生中的request對象,this.req.url則是node原生request中獲取url的方法。

2. response.js

response.js 也是對http模塊的response對象進行封裝,通過對response對象的某些屬性或方法通過getter/setter函數進行代理。

同理我們需要在我們項目的根目錄下新建一個response.js。基本代碼像如下所示:

const response = {
  get body() {
    return this._body;
  },
  set body(data) {
    this._body = data;
  },
  get status() {
    return this.res.statusCode;
  },
  set status(statusCode) {
    if (typeof statusCode !== 'number') {
      throw new Error('statusCode 必須為一個數字');
    }
    this.res.statusCode = statusCode;
  }
};

module.exports = response;

代碼也是如上一些簡單的代碼,該文件中有四個方法,分別是 body讀取和設置方法。讀取一個名為 this._body 的屬性。
status方法分別是設置或讀取 this.res.statusCode。同理:this.res是node原生中的response對象。

3. context.js

如上是簡單的 request.js 和 response.js ,那么context的核心是將 request, response對象上的屬性方法代理到context對象上。也就是說 將會把 this.res.statusCode 就會變成 this.ctx.statusCode 類似於這樣的代碼。request.js和response.js 中所有的方法和屬性都能在ctx對象上找到。

因此我們需要在項目中的根目錄下新建 context.js, 基本代碼如下:

const context = {
  get url() {
    return this.request.url;
  },
  set url(val) {
    this.request.url = val;
  },
  get body() {
    return this.response.body;
  },
  set body(data) {
    this.response.body = data;
  },
  get status() {
    return this.response.statusCode;
  },
  set status(statusCode) {
    if (typeof statusCode !== 'number') {
      throw new Error('statusCode 必須為一個數字');
    }
    this.response.statusCode = statusCode;
  }
};

module.exports = context;

如上代碼可以看到context.js 是做一些常用方法或屬性的代理,比如通過 context.url 直接代理了 context.request.url.
context.body 代理了 context.response.body, context.status 代理了 context.response.status. 但是 context.request、context.response會在application.js中掛載的。

注意:想要了解 getter/setter 的代理原理可以看這篇文章.

如上是簡單的代理,但是當有很多代理的時候,我們一個個編寫有點繁瑣,因此我們可以通過 __defineSetter__ 和 __defineGetter__來實現,該兩個方法目前不建議使用,我們也可以通過Object.defineProperty這個來監聽對象。
但是目前在koa2中還是使用 delegates模塊中的 __defineSetter__ 和 __defineGetter來實現的。delegates模塊它的作用是將內部對象上的變量或函數委托到外部對象上。具體想要了解 delegates模塊 請看我這篇文章。

因此我們的context.js 代碼可以改成如下(當然我們需要引入delegates模塊中的代碼引入進來);

const delegates = require('./delegates');

const context = {
  // ..... 其他很多代碼
};
// 代理request對象
delegates(context, 'request').access('url');

// 代理response對象
delegates(context, 'response').access('body').access('status');

/*
const context = {
  get url() {
    return this.request.url;
  },
  set url(val) {
    this.request.url = val;
  },
  get body() {
    return this.response.body;
  },
  set body(data) {
    this.response.body = data;
  },
  get status() {
    return this.response.statusCode;
  },
  set status(statusCode) {
    if (typeof statusCode !== 'number') {
      throw new Error('statusCode 必須為一個數字');
    }
    this.response.statusCode = statusCode;
  }
};
*/
module.exports = context;

如上代碼引入了 delegates.js 模塊,然后使用該模塊下的access的方法,該方法既擁有setter方法,也擁有getter方法,因此代理了request對象中的url方法,同時代理了context對象中的response屬性中的 body 和 status方法。

最后我們需要來修改application.js代碼,引入request,response,context對象。如下代碼:

const Emitter = require('events');
const http = require('http');

// 引入 context request, response 模塊
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Application extends Emitter {
  /* 構造函數 */
  constructor() {
    super();
    this.callbackFunc = null;
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
  // 開啟 http server 並且傳入參數 callback
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  use(fn) {
    this.callbackFunc = fn;
  }
  callback() {
    return (req, res) => {
      // this.callbackFunc(req, res);
      // 創建ctx
      const ctx = this.createContext(req, res);
      // 響應內容
      const response = () => this.responseBody(ctx);
      this.callbackFunc(ctx).then(response);
    }
  }
  /*
   構造ctx
   @param {Object} req實列
   @param {Object} res 實列
   @return {Object} ctx實列
  */
  createContext(req, res) {
    // 每個實列都要創建一個ctx對象
    const ctx = Object.create(this.context);
    // 把request和response對象掛載到ctx上去
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }
  /*
   響應消息
   @param {Object} ctx 實列
  */
  responseBody(ctx) {
    const content = ctx.body;
    if (typeof content === 'string') {
      ctx.res.end(content);
    } else if (typeof content === 'object') {
      ctx.res.end(JSON.stringify(content));
    }
  }
}

module.exports = Application;

如上代碼可以看到在callback()函數內部,我們把之前的這句代碼 this.callbackFunc(req, res); 注釋掉了,改成如下代碼:

// 創建ctx
const ctx = this.createContext(req, res);
// 響應內容
const response = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(response);

1. 首先是使用 createContext() 方法來創建ctx。然后把request對和response對象都直接掛載到了 ctx.request 和 ctx.response上,並且還將node原生的req/res對象掛載到了 ctx.request.req/ctx.req 和 ctx.response.res/ctx.res上了。

我們再來看下 request.js 的代碼:

const request = {
  get url() {
    return this.req.url;
  },
  set url(val) {
    this.req.url = val;
  }
};

module.exports = request;

我們之前request.js 代碼是如上寫的,比如 get url() 方法,返回的是 this.req.url, this.req是從什么地方來的?之前我們並不理解,現在我們知道了。

1. 首先我們把request掛載到ctx實列上了,如代碼:ctx.request = Object.create(this.request);然后node中的原生的req也掛載到ctx.req中了,如代碼:ctx.req = ctx.request.req = req; 因此request.js 中的this指向了createContext方法中掛載到了對應的實例上。因此 this.req.url 實際上就是 ctx.req.url了。同理 this.res 也是一樣的道理的。

2. 其次,我們使用 const response = () => this.responseBody(ctx); 該方法把ctx實列作用參數傳入 responseBody方法內作為
響應內容。代碼如下:

responseBody(ctx) {
  const content = ctx.body;
  if (typeof content === 'string') {
    ctx.res.end(content);
  } else if (typeof content === 'object') {
    ctx.res.end(JSON.stringify(content));
  }
}

如上我們創建了 responseBody方法,該方法的作用是通過ctx.body讀取信息,判斷該 ctx.body是否是字符串或對象,如果是對象的話,也會把它轉為字符串,最后調用 ctx.res.end() 方法返回信息並關閉連接。

3. 最后我們調用該代碼:this.callbackFunc(ctx).then(response); this.callbackFunc()函數就是我們使用koa中傳入的方法,比如如下koa代碼:

app.use(async ctx => {
  console.log(ctx.status); // 打印狀態碼為200
  ctx.body = 'hello world';
});

該回調函數是一個async函數,然后返回給我們的參數是ctx對象,async函數返回的是一個promise對象,因此在源碼中我們繼續調用then方法,把返回的內容掛載到ctx上。因此我們可以拿着ctx對象做我們自己想要做的事情了。

三:中間件機制的實現。

koa中的中間件是洋蔥型模型。具體的洋蔥模型的機制可以看這篇文章。

koa2中使用了async/await作為執行方式,具體理解 async/await的含義可以看我這篇文章介紹。

koa2中的中間件demo如下:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log(11111);
  await next();
  console.log(22222);
});

app.use(async (ctx, next) => {
  console.log(33333);
  await next();
  console.log(44444);
});

app.use(async (ctx, next) => {
  console.log(55555);
  await next();
  console.log(66666);
});
app.listen(3001);
console.log('app started at port 3000...');

// 執行結果為 11111  33333 55555 66666 44444 22222

如上執行結果為 11111 33333 55555 66666 44444 22222,koa2中的中間件模型為洋蔥型模型,當對象請求過來的時候,會依次經過各個中間件進行處理,當碰到 await next()時候就會跳到下一個中間件,當中間件沒有 await next執行的時候,就會逆序執行前面的中間件剩余的代碼,因此,先打印出 11111,然后碰到await next()函數,所以跳到下一個中間件去,就接着打印33333, 然后又碰到 await next(),因此又跳到下一個中間件,因此會打印55555, 打印完成后,繼續碰到 await next() 函數,但是后面就沒有中間件了,因此執行打印66666,然后逆序打印后面的數據了,先打印44444,執行完成后,就往上打印22222.

逆序如果我們不好理解的話,我們繼續來看下如下demo就能明白了。

function test1() {
  console.log(1)
  test2();
  console.log(5)
  return Promise.resolve();
}
function test2() {
  console.log(2)
  test3();
  console.log(4)
}

function test3() {
  console.log(3)
  return;
}
test1();

如上代碼打印的順序分別為 1, 2, 3, 4, 5; 上面的代碼就是和koa2中的中間件逆序順序是一樣的哦。可以自己理解一下。

那么現在我們想要實現這么一個類似koa2中間件的這么一個機制,我們該如何做呢?

我們都知道koa2中是使用了async/await來做的,假如我們現在有如下三個簡單的async函數:

// 假如下面是三個測試函數,想要實現 koa中的中間件機制
async function fun1(next) {
  console.log(1111);
  await next();
  console.log('aaaaaa');
}

async function fun2(next) {
  console.log(22222);
  await next();
  console.log('bbbbb');
}

async function fun3() {
  console.log(3333);
}

如上三個簡單的函數,我現在想構造出一個函數,讓這三個函數依次執行,先執行fun1函數,打印1111,然后碰到 await next() 后,執行下一個函數 fun2, 打印22222, 再碰到 await next() 就執行fun3函數,打印3333,然后繼續打印 bbbbb, 再打印 aaaaa。

因此我們需要從第一個函數入手,因為首先打印的是 11111, 因此我們需要構造一個調用 fun1函數了。fun1函數的next參數需要能調用 fun2函數了,fun2函數中的next參數需要能調用到fun3函數了。因此代碼改成如下:

// 假如下面是三個測試函數,想要實現 koa中的中間件機制
async function fun1(next) {
  console.log(1111);
  await next();
  console.log('aaaaaa');
}

async function fun2(next) {
  console.log(22222);
  await next();
  console.log('bbbbb');
}

async function fun3() {
  console.log(3333);
}

let next1 = async function () {
  await fun2(next2);
}
let next2 = async function() {
  await fun3();
}
fun1(next1);

然后我們執行一下,就可以看到函數會依次執行,結果為:1111,22222,3333,bbbbb, aaaaaa;

如上就可以讓函數依次執行了,但是假如頁面有n個中間件函數,我們需要依次執行怎么辦呢?因此我們需要抽象成一個公用的函數出來,據koa2中application.js 源碼中,首先會把所有的中間件函數放入一個數組里面去,比如源碼中這樣的:
this.middleware.push(fn); 因此我們這邊首先也可以把上面的三個函數放入數組里面去,然后使用for循環依次循環調用即可:

如下代碼:

async function fun1(next) {
  console.log(1111);
  await next();
  console.log('aaaaaa');
}

async function fun2(next) {
  console.log(22222);
  await next();
  console.log('bbbbb');
}

async function fun3() {
  console.log(3333);
}

function compose(middleware, oldNext) {
  return async function() {
    await middleware(oldNext);
  }
}

const middlewares = [fun1, fun2, fun3];

// 最后一個中間件返回一個promise對象
let next = async function() {
  return Promise.resolve();
};

for (let i = middlewares.length - 1; i >= 0; i--) {
  next = compose(middlewares[i], next);
}
next();

最后依次會打印 1111 22222 3333 bbbbb aaaaaa了。

如上代碼是怎么執行的呢?首先我們會使用一個數組 middlewares 保存所有的函數,就像和koa2中一樣使用 app.use 后,會傳入async函數進去,然后會依次通過 this.middlewares 把對應的函數保存到數組里面去。然后我們從數組末尾依次循環該數組最后把返回的值保存到 next 變量里面去。如上代碼:

因此for循環第一次打印 middlewares[i], 返回的是 fun3函數,next傳進來的是 async function { return Promise.resolve()} 這樣的函數,最后返回該next,那么此時該next保存的值就是:

next = async function() {
  await func3(async function(){
    return Promise.resolve();
  });
}

for 循環第二次的時候,返回的是 fun2函數,next傳進來的是 上一次返回的函數,最后返回next, 那么此時next保存的值就是

next = async function() {
  await func2(async function() {
    await func3(async function(){
      return Promise.resolve();
    });
  });
}

for循環第三次的時候,返回的是 fun1 函數,next傳進來的又是上一次返回的async函數,最后也返回next,那么此時next的值就變為:

next = async function(){
  await fun1(async function() {
    await fun2(async function() {
      await fun3(async function(){
        return Promise.resolve();
      });
    });
  });
};

因此我們下面調用 next() 函數的時候,會依次執行 fun1 函數,執行完成后,就會調用 fun2 函數,再執行完成后,接着調用fun3函數,依次類推..... 最后一個函數返回 Promise.resolve() 中Promise成功狀態。

如果上面的async 函數依次調用不好理解的話,我們可以繼續看如下demo;代碼如下:

async function fun1(next) {
  console.log(1111);
  await next();
  console.log('aaaaaa');
}

async function fun2(next) {
  console.log(22222);
  await next();
  console.log('bbbbb');
}

async function fun3() {
  console.log(3333);
}

const next = async function(){
  await fun1(async function() {
    await fun2(async function() {
      await fun3(async function(){
        return Promise.resolve();
      });
    });
  });
};

next();

最后結果也會依次打印 1111, 22222, 3333, bbbbb, aaaaaa;

因此上面就是我們的koa2中間件機制了。我們現在把我們總結的機制運用到我們application.js中了。因此application.js代碼變成如下:

const Emitter = require('events');
const http = require('http');

// 引入 context request, response 模塊
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Application extends Emitter {
  /* 構造函數 */
  constructor() {
    super();
    // this.callbackFunc = null;
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 保存所有的中間件函數
    this.middlewares = [];
  }
  // 開啟 http server 並且傳入參數 callback
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  use(fn) {
    // this.callbackFunc = fn;
    // 把所有的中間件函數存放到數組里面去
    this.middlewares.push(fn);
    return this;
  }
  callback() {
    return (req, res) => {
      // this.callbackFunc(req, res);
      // 創建ctx
      const ctx = this.createContext(req, res);
      // 響應內容
      const response = () => this.responseBody(ctx);
      //調用 compose 函數,把所有的函數合並
      const fn = this.compose();
      return fn(ctx).then(response);
    }
  }
  /*
   構造ctx
   @param {Object} req實列
   @param {Object} res 實列
   @return {Object} ctx實列
  */
  createContext(req, res) {
    // 每個實列都要創建一個ctx對象
    const ctx = Object.create(this.context);
    // 把request和response對象掛載到ctx上去
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }
  /*
   響應消息
   @param {Object} ctx 實列
  */
  responseBody(ctx) {
    const content = ctx.body;
    if (typeof content === 'string') {
      ctx.res.end(content);
    } else if (typeof content === 'object') {
      ctx.res.end(JSON.stringify(content));
    }
  }
  /*
   把傳進來的所有的中間件函數合並為一個中間件
   @return {function}
  */
  compose() {
    // 該函數接收一個參數 ctx
    return async ctx => {
      function nextCompose(middleware, oldNext) {
        return async function() {
          await middleware(ctx, oldNext);
        }
      }
      // 獲取中間件的長度
      let len = this.middlewares.length;
      // 最后一個中間件返回一個promise對象
      let next = async function() {
        return Promise.resolve();
      };
      for (let i = len; i >= 0; i--) {
        next = nextCompose(this.middlewares[i], next);
      }
      await next();
    };
  }
}

module.exports = Application;

1. 如上代碼在構造函數內部 constructor 定義了一個變量 this.middlewares = []; 目的是保存app.use(fn)所有的中間件函數,

2. 然后我們在use函數內部,不是把fn賦值,而是把fn放到一個數組里面去,如下代碼:

use(fn) {
  // this.callbackFunc = fn;
  // 把所有的中間件函數存放到數組里面去
  this.middlewares.push(fn);
  return this;
}

3. 最后把所有的中間件函數合並為一個中間件函數;如下compose函數的代碼如下:

compose() {
  // 該函數接收一個參數 ctx
  return async ctx => {
    function nextCompose(middleware, oldNext) {
      return async function() {
        await middleware(ctx, oldNext);
      }
    }
    // 獲取中間件的長度
    let len = this.middlewares.length;
    // 最后一個中間件返回一個promise對象
    let next = async function() {
      return Promise.resolve();
    };
    for (let i = len; i >= 0; i--) {
      next = nextCompose(this.middlewares[i], next);
    }
    await next();
  };
}

該compose函數代碼和我們之前的demo代碼是一樣的。這里就不多做解析哦。

4. 在callback函數內部改成如下代碼:

callback() {
  return (req, res) => {
    /*
    // 創建ctx
    const ctx = this.createContext(req, res);
    // 響應內容
    const response = () => this.responseBody(ctx);
    this.callbackFunc(ctx).then(response);
    */
    // 創建ctx
    const ctx = this.createContext(req, res);
    // 響應內容
    const response = () => this.responseBody(ctx);
    //調用 compose 函數,把所有的函數合並
    const fn = this.compose();
    return fn(ctx).then(response);
  }
}

如上代碼和之前版本的代碼,最主要的區別是 最后兩句代碼,之前的是直接把fn函數傳入到 this.callbackFunc函數內。現在是使用 this.compose()函數調用,把所有的async的中間件函數合並成一個中間件函數后,把返回的合並后的中間件函數fn再去調用,這樣就會依次調用和初始化各個中間件函數,具體的原理機制我們上面的demo已經講過了,這里就不再多描述了。

最后我們需要一個測試文件,來測試該代碼:如下在test.js 代碼如下:

const testKoa = require('./application');
const app = new testKoa();

const obj = {};

app.use(async (ctx, next) => {
  obj.name = 'kongzhi';
  console.log(1111);
  await next();
  console.log('aaaaa');
});

app.use(async (ctx, next) => {
  obj.age = 30;
  console.log(2222);
  await next();
  console.log('bbbbb')
});

app.use(async (ctx, next) => {
  console.log(3333);
  console.log(obj);
});
app.listen(3001, () => {
  console.log('listening on 3001');
});

我們運行下即可看到,在命令行中會依次打印如下所示:

如上是先打印1111,2222,3333,{'name': 'kongzhi', 'age': 30}, bbbbb, aaaaa.

因此如上就是koa2中的中間件機制了。

四:錯誤捕獲和錯誤處理。

一個非常不錯的框架,當異常的時候,都希望能捕獲到該異常,並且希望把該異常返回給客戶端,讓開發者知道異常的一些信息。

比如koa2中的異常情況下,會報錯如下信息:demo如下:

const Koa = require('koa');
const app = new Koa();

app.use((ctx) => {
  str += 'hello world'; // 沒有聲明該變量, 所以直接拼接字符串會報錯
  ctx.body = str;
});

app.on('error', (err, ctx) => { // 捕獲異常記錄錯誤日志
  console.log(err);
});

app.listen(3000, () => {
  console.log('listening on 3000');
});

如上代碼,由於str是一個未定義的變量,因此和字符串拼接的時候會報錯,但是koa2中我們可以使用 app.on('error', (err, ctx) => {}) 這樣的error方法來進行監聽的。因此在命令行中會報如下錯誤提示:

因此我們現在也是一樣,我們需要有對於某個中間件發生錯誤的時候,我們需要監聽error這個事件進行監聽。
因此我們需要定義一個onerror函數,當發生錯誤的時候,我們可以使用Promise中的catch方法來捕獲該錯誤了。

因此我們可以讓我們的Application繼承於Event這個對象,在koa2源碼中的application.js 中有 onerror函數,我們把它復制到我們的Application.js 中,代碼如下:

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();
}

然后在我們我們的callback()函數中最后一句代碼使用catch去捕獲這個異常即可:代碼如下:

callback() {
  return (req, res) => {
    /*
    // 創建ctx
    const ctx = this.createContext(req, res);
    // 響應內容
    const response = () => this.responseBody(ctx);
    this.callbackFunc(ctx).then(response);
    */
    // 創建ctx
    const ctx = this.createContext(req, res);
    // 響應內容
    const response = () => this.responseBody(ctx);

    // 響應時 調用error函數
    const onerror = (err) => this.onerror(err, ctx);

    //調用 compose 函數,把所有的函數合並
    const fn = this.compose();
    return fn(ctx).then(response).catch(onerror);
  }
}

因此Application.js 所有代碼如下:

const Emitter = require('events');
const http = require('http');

// 引入 context request, response 模塊
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Application extends Emitter {
  /* 構造函數 */
  constructor() {
    super();
    // this.callbackFunc = null;
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 保存所有的中間件函數
    this.middlewares = [];
  }
  // 開啟 http server 並且傳入參數 callback
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  use(fn) {
    // this.callbackFunc = fn;
    // 把所有的中間件函數存放到數組里面去
    this.middlewares.push(fn);
    return this;
  }
  callback() {
    return (req, res) => {
      /*
      // 創建ctx
      const ctx = this.createContext(req, res);
      // 響應內容
      const response = () => this.responseBody(ctx);
      this.callbackFunc(ctx).then(response);
      */
      // 創建ctx
      const ctx = this.createContext(req, res);
      // 響應內容
      const response = () => this.responseBody(ctx);

      // 響應時 調用error函數
      const onerror = (err) => this.onerror(err, ctx);

      //調用 compose 函數,把所有的函數合並
      const fn = this.compose();
      return fn(ctx).then(response).catch(onerror);
    }
  }
  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */

  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();
  }
  /*
   構造ctx
   @param {Object} req實列
   @param {Object} res 實列
   @return {Object} ctx實列
  */
  createContext(req, res) {
    // 每個實列都要創建一個ctx對象
    const ctx = Object.create(this.context);
    // 把request和response對象掛載到ctx上去
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }
  /*
   響應消息
   @param {Object} ctx 實列
  */
  responseBody(ctx) {
    const content = ctx.body;
    if (typeof content === 'string') {
      ctx.res.end(content);
    } else if (typeof content === 'object') {
      ctx.res.end(JSON.stringify(content));
    }
  }
  /*
   把傳進來的所有的中間件函數合並為一個中間件
   @return {function}
  */
  compose() {
    // 該函數接收一個參數 ctx
    return async ctx => {
      function nextCompose(middleware, oldNext) {
        return async function() {
          await middleware(ctx, oldNext);
        }
      }
      // 獲取中間件的長度
      let len = this.middlewares.length;
      // 最后一個中間件返回一個promise對象
      let next = async function() {
        return Promise.resolve();
      };
      for (let i = len; i >= 0; i--) {
        next = nextCompose(this.middlewares[i], next);
      }
      await next();
    };
  }
}

module.exports = Application;

然后我們使用test.js 編寫測試代碼如下:

const testKoa = require('./application');
const app = new testKoa();

app.use((ctx) => {
  str += 'hello world'; // 沒有聲明該變量, 所以直接拼接字符串會報錯
  ctx.body = str;
});

app.on('error', (err, ctx) => { // 捕獲異常記錄錯誤日志
  console.log(err);
});

app.listen(3000, () => {
  console.log('listening on 3000');
});

當我們在瀏覽器訪問的 http://localhost:3000/ 的時候,我們可以在命令行中看到如下報錯信息了:

總結:如上就是實現一個簡單的koa2框架的基本原理,本來想把koa2源碼也分析下,但是篇幅有限,所以下篇文章繼續把koa2所有的源碼簡單的解讀下,其實看懂這篇文章后,已經可以理解95%左右的koa2源碼了,只是說koa2源碼中,比如request.js 會包含更多的方法,及 response.js 包含更多有用的方法等。

github源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM