《基于Koa我们到底能干啥》之了解Koa

1,857 阅读5分钟

前言

之所以想到写《基于Koa我们到底能干啥》是因为对于node来说现在大部分前端都还只是停留在如何使用的情况下,并没有像研究Vue、React等等一样去研究node,但是node对于我们前端的发展是必不可少的,因为他是一个服务于前端的后端,首先node肯定不会分配专业的后端去开发node,只能前端自己去开发,那么面对根本没有深入了解过node的同学来说之无疑是很难受的,所以我觉得需要这么一篇文章来带大家去接触node。

选择了Koa而不是express的原因是,express大多数的api已经封装的比较完善了,但是企业级应用需要的是良好的可扩展性,显而易见Koa更加适合。(其实是因为上一篇文章立了一个flag要写koa)

koa源码解析

Koa简介

Koa的设计非常精巧,内置的概念只有5个,如下:

  • Application:应用
  • Context:上下文
  • Request:请求
  • Response:响应
  • Middleware:中间件

基于Koa编写应用时,我们会做以下两类事情:

  • 编写中间件来处理请求
  • 在Application实例、Context实例、Request实例或者Response实例上定义一些工具方法或属性,以便在中间件中使用。 所以,编写Koa应用其实就是在Application、Context、Request、Response、Middleware等五个不同的维度去增强Koa的能力。

中间件

Koa 中间件是一个很重要的概念,koa的中间件类似洋葱模型。如下:

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

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

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

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

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

输出为1 3 5 6 4 2

Koa运行流程的三两事

初始化Koa

Koa应用中,通过const app = new Koa();来构建一个应用实例。Koa的constructot中的主要步骤如下:

constructor() {
    super();
    this.middleware = [];
    this.context = Object.create(context); // 一些方法等,如:assert、onerror、throw
    this.request = Object.create(request); // 一些属性等,如:headers、url、path
    this.response = Object.create(response); // 一些方法等,如:body、redirect
}

初始化的过程就是添加相应的属性和方法并初始化这些方法和属性。

添加中间件

Koa应用中新增中间件可以直接通过app上挂载的use()方法,本质上app.use()只是将中间件push到中间件队列中的一个函数,如下

function use(middleware) {
    if (typeof middleware !== 'function') throw new TypeError('middleware must be a function!');
    // .....
    this.middlewares.push(middleware);
    return this;
}

监听端口

Koa应用实例调用app.listen(端口号))方法监听端口,监听端口的回调函数其实是handleRequest,这个方法会创建ctx,并把ctx和中间件函数一起传入this.handleRequest(ctx, fn)。这个方法主要是用来在请求到来时,把请求中的参数赋值给ctx,并依次调用middleware来对请求做处理。至此,Koa应用启动完毕。

function listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
function callback() {
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
        const ctx = this.createContext(req, res);
        return this.handleRequest(ctx, fn);
    };
    return handleRequest;
}
4. 

在以上代码中const fn = compose(this.middleware);通过compose实现依次调用middleware来对请求做处理,这也是koa中洋葱模型实现的关键,代码如下:

function compose (middleware) {
 if (typeof middleware !== 'function') throw new TypeError('middleware must be a function!');
 return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

处理请求

从listen函数中可以知道,监听的端口监听到请求时,会依次调用this.createContext(req, res)this.handleRequest(ctx, fn)。前者主要是监听请求头部的值然后初始化header、context等属性同时也能通过req来保证context.request正确的格式和初始化。

function createContext(req, res) {
    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;
    context.originalUrl = request.originalUrl = req.url;
    // 在自己开发业务框架的时候可以新增一些业务相关的属性以便于后面的调用
    context.state = {};
    return context;
}

对于this.handleRequest(ctx, fn)来说更多的是在把用到的中间件串联起来,handleRequest函数,在执行一遍中间件函数后,就执行handleResponse函数。中间函数是我们的业务逻辑,里面可能有设置响应头、打印日志、设置response.body等功能。而handleResponse则负责把ctx.response转成相应的http response,然后返回。

function handleRequest(ctx, Middleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return Middleware(ctx)
        .then(handleResponse)
        .catch(onerror);
}

总结

总结一些Koa的一些特性吧,首先Koa的涉及十分轻巧适合做二次封装,面对复杂的业务场景有着丰富的可扩展性,特别是在企业级开发中多模块多依赖复用的场景,但是它也有一定的弊端,在多人开发中由于没有统一的代码规范容易造成代码混乱结构不清晰的情况,考虑到种种情况我们可以对Koa进行二次封装,封装成相应的业务框架。同时就算不做业务框架也可以基于Koa源码来对Koa进行一些魔改增加一些适合业务开发的方法和属性。总之,Koa只是万丈高楼的一块地基,如何去建造这幢高楼取决于建筑师,Koa已经提供了很高的自由度,怎么去做就取决于你了。


大家也不妨一起来思考一下,基于这一个轻巧的设计我们到底能做哪些改造来让它更加适合我们的业务场景,欢迎大家踊跃发言,我的想法和实践将在下一篇文章分享。