探究 Koa 内部原理

550 阅读8分钟

koa 的官方简介

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

koa源码目录

image.png

koa的源码十分简洁,lib文件夹下只有四个js文件,分别为:

  • application.js: 主干部分,在这里进行了中间件合并、上下文封装、处理请求&响应、错误监听等操作。
  • context.js: 代表上下文ctx
  • request.js: 封装ctx.request的逻辑,是对原生req的扩展
  • response.js: 封装ctx.response的逻辑,是对原生res的扩展

简单体验koa

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

app.use((ctx) => {
  console.log(123);
  ctx.body = "<h1>hello world !</h1>";
});

app.listen(3000, () => {
  console.log("server start 3000");
});

页面显示:

image.png

从上面简单的演示,可以看出koa导出的是一个类,这个类有use,listen等方法.

use可以接收一个函数来处理http请求,listen用来指定服务跑在哪个端口,并且有一个成功的回调

开始实现mini-koa

我们也创建一个lib文件,里面有application.js context.js request.js response.js,和koa源码保持一直

image.png

application.js入口文件我们创建一个Application类,先把架子搭好

class Application {
  constructor() {}
  use() {}
  listen() {}
}
module.exports = Application;

我们都知道koa是对原生http模块的封装,所以listen方法实现很简单

  handleRequest(req, res) {}
  listen() {
    const server = http.createServer(this.handleRequest); //this.handleRequest来处理请求
    server.listen(...arguments);
  }

但是注意这里会有一个问题createServer传入的函数this默认指向的是http创建的server,我们实际让this指向application的实例,所以这里需要改造一下,改造的方式很多,我们选择bind函数

const server = http.createServer(this.handleRequest.bind(this)); //this.handleRequest来处理请求

listen实现完了,我们再来实现usehandleRequest.我么都知道use方法可以处理多个函数,通过调用next来执行下一个函数,例如

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

app.use((ctx, next) => {
  ctx.body = "1";
  next();
});
app.use((ctx, next) => {
  ctx.body = ctx.body + "2";
  next();
});
app.use((ctx) => {
  ctx.body = ctx.body + "3";
});

app.listen(3000, () => {
  console.log("server start 3000");
});

但是我们首先处理简单的情况,use只能处理一个函数

我们在context.js request.js response.js文件下都导出一个空对象{},

//context.js request.js response.js
module.exports = {};

注意

context 上下文对象不能够共享,每个new Koa()的实例的context不能一样,每次http请求的context也不能一样,也就是不能引用同一块地址.

  1. 每个应用独立的上下文
  2. 每次请求独立的上下文
// application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application {
  constructor() {
    //1.每个应用独立的上下文

    //this.context.__proto__ = context
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
  use(fn) {
    this.fn = fn; //先实现简单的,假设use只能处理一个函数
  }
  createContext(req, res) {
    const ctx = Object.create(this.context);
    const request = Object.create(this.request);
    const response = Object.create(this.response);

    //ctx的request和response对象
    ctx.request = request;
    ctx.response = response;

    //原生的req,res保存
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.request.res = res;

    return ctx;
  }
  handleRequest(req, res) {
    // 2.每次请求都产生新的上下文
    const ctx = this.createContext(req, res);
    this.fn(ctx);
  }
  listen() {
    const server = http.createServer(this.handleRequest.bind(this)); //this.handleRequest来处理请求
    server.listen(...arguments);
  }
}
module.exports = Application;
//request.js
const parse = require("parseurl");
module.exports = {
  //获取path方法
  get path() {
    return parse(this.req).pathname;
  },
};
const Koa = require("../koa");
const app = new Koa();

app.use((ctx) => {
  console.log(ctx.request.path);
});

app.listen(3000, () => {
  console.log("server start 3000");
});

这样通过原型链就可以找到属性,又可以使每个context对象独立

通过访问ctx.request.path,进入get path() 方法,此时this指向ctx.request,又因为先前把req挂载到了ctx.request,所以return parse(this.req).pathname;this.req获取的是当前请求原生的req对象

这就是为什么request身上为什么加上一个req属性,为了在取值的时候可以快速获取到原生的req

测试一下:

访问http://localhost:3000/test,控制台输出/test

有了这样的"套路"我们就可以不停的给request和response上扩展不同的功能

比如实现ctx.request.headers

//request.js
const parse = require("parseurl");
module.exports = {
  //获取path方法
  get path() {
    return parse(this.req).pathname;
  },
  get headers() {
    return this.req.headers;
  },
};

然而ctx.path也能获取到path值,那是怎么实现的呢?

我们可以这样实现它

// context.js
module.exports = {
  get path() {
    return this.request.path;
  },
};

测试一下发现没有问题,但是我们想一下如果ctx有100个属性,我们就要创建100个吗?

所以,我们可以考虑使用函数包装一下

例如:我们需要requestresponse定义的方法 委托到了 context 对象上,就向下面一样传入委托的targetkey

defineGetter("request", "path");
defineGetter("request", "headers");

我们可以用Object.defineProperty来实现

function defineGetter(target, key) {
  Object.defineProperty(proto, key, {
    get() {
      // 当在ctx读取值时,ctx上没有的属性会向原型找,就会触发get方法
      //this是ctx对象
      return this[target][key];
    },
  });
}

但是koa中实现使用的并不是Object.defineProperty而是一个delegate库,内部的原理是一个废弃的api__defineSetter__ ``

//koa源码
/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

再看下ctx.response.body怎么实现

//response.js
module.exports = {
  _body: undefined,
  get body() {
    return this._body;
  },
  set body(content) {
    this._body = content;
  },
};

// context.js
const proto = (module.exports = {});

function defineGetter(target, key) {
  Object.defineProperty(proto, key, {
    get() {
      // 当在ctx读取值时,ctx上没有的属性会向原型找,就会触发get方法
      //this是ctx对象
      return this[target][key];
    },
  });
}
function defineGetterAndSetter(target, key) {
  Object.defineProperty(proto, key, {
    set(newValue) {
      this[target][key] = newValue;
    },
    get() {
      return this[target][key];
    },
  });
}
defineGetter("request", "path");
defineGetter("request", "headers");

defineGetterAndSetter("response", "body");

//测试

const Koa = require("../koa");
const app = new Koa();

app.use((ctx) => {
  // ctx.body和ctx.request.body可以多次调用,并且可以获取
  ctx.body = "hello";
  ctx.body = "world";
  ctx.response.body = "hello world";
  console.log(ctx.body); //打印结果:hello world
});

app.listen(3000, () => {
  console.log("server start 3000");
});

console.log(ctx.body); //打印结果:hello world

*但是页面没有响应

需要修改一下handleRequest方法

//application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application {
  constructor() {...}
  use(fn) {...}
  createContext(req, res) {...}
  handleRequest(req, res) {
    // 2.每次请求都产生新的上下文
    const ctx = this.createContext(req, res);
    res.statusCode = 404; //默认404
    this.fn(ctx);
    let body = ctx.body;
    //如果有值返回信息最终结果响应给用户
    if (body != null) {
      res.statusCode = 200;
      res.end(ctx.body);
    } else {
      res.end("Not Found");
    }
  }
  listen() {...}
}
module.exports = Application;

这样我们就完成了一个只能处理一个函数的koa

然而 Koa 最有亮点的就是 洋葱模型

koa 洋葱模型

image.png

image.png

//测试

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

app.use((ctx, next) => {
  console.log(1);
  next(); // next 表示下一个中间件
  console.log(2);
});
app.use((ctx, next) => {
  console.log(3);
  next();
  console.log(4);
});
app.use((ctx, next) => {
  console.log(5);
  next();
  console.log(6);
});

app.listen(3000, () => {
  console.log("server start 3000");
});

打印顺序1 3 5 6 4 2,调用next就会进入下一个中间件,就像剥洋葱一样 (1(3(5 6)4)2)

那么中间件有异步事件呢?

  • koa 中所有的异步操作都要基于promise
  • koa 内部会将所有的中间件组合操作 组合成立一个大的 promise 只要从头走到结束就算完成了
  • koa 中所有中间件都要加await next()或者return next(),否则异步逻辑可能出错
function sleep() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("sleep");
    }, 500);
  });
}
app.use((ctx, next) => {
  console.log(1);
  ctx.body = "1";
  next(); // next 表示下一个中间件
  console.log(2);
  ctx.body = "2";
});
app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = "3";
  await sleep();
  next();
  console.log(4);
  ctx.body = "4";
});
app.use((ctx, next) => {
  console.log(5);
  ctx.body = "5";
  next();
  console.log(6);
  ctx.body = "6";
});

输出顺序1 3 2 5 6 4

next如果没有await,不会等待下一个中间件中异步执行完

实现多个use

constructor中添加this.middlewares = [];,每次use时this.middlewares.push(fn)

新增一个compose函数用来合并所有中间件,compose返回的是一个promise

 constructor() {
    //1.每个应用独立的上下文

    //this.context.__proto__ = context
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 存放所有middleware的数组
    this.middlewares = []; //+
  },
   use(fn) {
    this.middlewares.push(fn);
  },
  handleRequest(req, res) {
    // 2.每次请求都产生新的上下文
    const ctx = this.createContext(req, res);
    res.statusCode = 404; //默认404
    // 组合所有middleware
    this.compose(ctx).then(() => {
      let body = ctx.body;
      //如果有值返回信息最终结果响应给用户
      if (body != null) {
        res.statusCode = 200;
        res.end(ctx.body);
      } else {
        res.end("Not Found");
      }
    });
  }
    //compose
  compose(ctx) {
    let index = -1; //记录上一次调用索引
    const dispath = (i) => {
      if (i <= index) {
        // 如果当前i <= index,说明重复调用,报错
        throw new Error("next() 调用多次");
      }
      index = i;
      if (i === this.middlewares.length) {
        //如果是最后一个中间件调用的next直接返回Promise.resolve()
        return Promise.resolve();
      }
      const middleware = this.middlewares[i]; //当前middleware
      // try catch 下 middleware执行可能报错,可以捕获错误,一起处理
      try {
        return Promise.resolve(middleware(ctx, () => dispath(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    };
    return dispath(0); //执行第一个中间件
  }

最后继承node中发布订阅events

const EventEmitter = require("events");

class Application extends EventEmitter

在中间件执行抛出的错误

    this.compose(ctx)
      .then(() => {
        let body = ctx.body;
        //如果有值返回信息最终结果响应给用户
        if (body != null) {
          res.statusCode = 200;
          res.end(ctx.body);
        } else {
          res.end("Not Found");
        }
      })
      .catch((err) => {
        this.emit("error", err);//发布
      });
const app = new Koa();
// 统一处理错误
app.on("error", (e) => {
  console.log(e);
});

koa 特点

  1. koa 中可以丰富ctx,而且内部提供了一个中间件流程
  2. 提供了更好的错误处理

代码

const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
const EventEmitter = require("events");
class Application extends EventEmitter {
  constructor() {
    super();
    //1.每个应用独立的上下文

    //this.context.__proto__ = context
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

    this.middlewares = []; //+
  }
  use(fn) {
    this.middlewares.push(fn);
  }
  createContext(req, res) {
    const ctx = Object.create(this.context);
    const request = Object.create(this.request);
    const response = Object.create(this.response);

    //ctx的request和response对象
    ctx.request = request;
    ctx.response = response;

    //原生的req,res保存
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.request.res = res;

    return ctx;
  }
  compose(ctx) {
    let index = -1; //记录上一次调用索引
    const dispath = (i) => {
      if (i <= index) {
        // 如果当前i <= index,说明重复调用,报错
        throw new Error("next() 调用多次");
      }
      index = i;
      if (i === this.middlewares.length) {
        //如果是最后一个中间件调用的next直接返回Promise.resolve()
        return Promise.resolve();
      }
      const middleware = this.middlewares[i]; //当前middleware
      // try catch 下 middleware执行可能报错,可以捕获错误,一起处理
      try {
        return Promise.resolve(middleware(ctx, () => dispath(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    };
    return dispath(0); //执行第一个中间件
  }
  handleRequest(req, res) {
    // 2.每次请求都产生新的上下文
    const ctx = this.createContext(req, res);
    res.statusCode = 404; //默认404
    // 组合所有middleware
    this.compose(ctx)
      .then(() => {
        let body = ctx.body;
        //如果有值返回信息最终结果响应给用户
        if (body != null) {
          res.statusCode = 200;
          res.end(ctx.body);
        } else {
          res.end("Not Found");
        }
      })
      .catch((err) => {
        this.emit("error", err);
      });
  }
  listen() {
    const server = http.createServer(this.handleRequest.bind(this)); //this.handleRequest来处理请求
    server.listen(...arguments);
  }
}
module.exports = Application;