node框架koa2的洋葱模型原理详解

·  阅读 1206

Koa -- 基于 Node.js 平台的下一代 web 开发框架

koa2 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用,可以免除重复繁琐的 回调函数 嵌套, 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何 中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。开发思路和express差不多,最大的特点就是可以避免异步嵌套。

koa2利用ES7的async/await特性,极大的解决了我们在做nodejs开发的时候异步给我们带来的烦恼,因此这也是 koa2 的一个核心概念 --- 洋葱模型

image.png

洋葱模型

洋葱模型采用的是先进后出的堆栈结构,加上async和await的特性,从而解决了地狱回调及由开发者更灵活的进行中间件组合,更好的对请求上下文的扩展。

先看一个示例:

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

app.use(async (cxt, next) => {
  console.log('middleware_1 start', 'test: ', cxt.test)
  cxt.test = 'middleware_1'
  await next()
  console.log('middleware_1 end', 'test: ', cxt.test)
})

app.use(async (cxt, next) => {
  console.log('middleware_2 start', 'test: ', cxt.test)
  cxt.test = 'middleware_2'
  await next()
  console.log('middleware_2 end', 'test: ', cxt.test)
})

app.use(async (cxt, next) => {
  console.log('middleware_3 start', 'test: ', cxt.test)
  cxt.test = 'middleware_3'
  console.log('middleware_3 end', 'test: ', cxt.test)
})

app.listen(3000)
复制代码

输出结果:

middleware_1 start test:  undefined
middleware_2 start test:  middleware_1
middleware_3 start test:  middleware_2
middleware_3 end test:  middleware_3
middleware_2 end test:  middleware_3
middleware_1 end test:  middleware_3
复制代码

以上代码中在use内放入了三个函数,函数内有两个参数,分别是cxt和next,在三个函数内分别给给test赋上不同的值,依次在next函数的上下分别执行打印,根据结果可以看到,每一个中间件的test值传递给了下一个中间件,根据上面的示例去看看源码是怎么实现的:

1、首先在 node_modules 文件夹中找到 koa2 的文件夹,并找到 package.json 中的 main 字段(该字段是整个组件包的入口文件路径)。

image.png 然后根据 main 字段显示的路径(lib/application.js),并打开。

2、根据上面示例,koa2 的需要 new 操作符返回一个实例,因此 application.js 肯定返回一个构造函数或者是类,然后还需要详细查看 use 方法及 listen 方法。

// xxx // 其它组件
const Emitter = require('events');  // 这是一个node的发布订阅模块
const http = require('http'); // http的请求模块
const compose = require('koa-compose');

module.exports = class Application extends Emitter {

  constructor(server) {
    super();

    this.server=server
    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
  
  listen() {
    debug('listen');
    const server =this.server || http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }
  
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will been removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x---deprecated');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
  
  // xx 其它方法
}
复制代码

先看 use 方法,除去 if 的边界判断,整个 use 方法其实就做了一件事情,就是用 constructor 内声明的一个 middlewre 变量来收集 use 传进来的所有中间件函数。

然后再看 listen 方法,里面做的事情是用 http 来启动一个端口服务,在启动之前,如果 constructor 在实例化的时候有传一个 server 对象就优先用,否则就用 http 来create一个新的 http server。在上面是示例中,实例化 Koa 的时候是没有传参数的,所以则需要看create逻辑,在 createServer 中有一个新的 callback 方法执行,然后去看下 callback 方法。

3、callback 方法

  callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

    return (req, res) => {
      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      const onerror = err => ctx.onerror(err);
      onFinished(res, onerror);
      fn(ctx).then(() => respond(ctx)).catch(onerror);
    };
  }
复制代码

在 callback 内部,首先是调用了一个 compose 方法得到一个 fn,等下去看 compose 方法(整个洋葱模型的核心),callback 返回一个函数,该函数也是上面 createServer 的回调函数,用于接收 http 请求的请求头与响应头,首先用内部的 createContext 方法处理请求头与响应头,然后将 ctx 传给用 compose 执行返回的函数 fn。

createContext 方法实际就是创建一个上下文 context,然后将请求头、响应头对象与 context 结合处理并返回,也就是 use 的回调函数的第一个参数:

  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.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
复制代码

还有一个 respond 函数,该函数就是所有中间都执行完成之后,将需要返回的内容返回给客户端:

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
复制代码

4、compose 方法

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    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, function next () {
          return dispatch(i + 1) // 执行下一个中间价
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码

compose 方法的参数即用 use 方法收集的所有中间件集合,并返回一个函数,该函数内部的 dispatch 方法是整个洋葱模型的核心实现。

从下标为 0 开始,依次执行所有中间件,在执行中间件的时候,将上下文参数 ctx 与带有内部执行下一个中间件的 next 方法传进去,依次执行完所有中间件,直到 fn 为空,返回一个空的 Promise.resolve 结束。

最后,将开始的示例转换为普通的函数嵌套执行:

async function fn1(cxt) {
  console.log('middleware_1 start', 'test: ', cxt.test)
  cxt.test = 'middleware_1'
  await fn2(cxt)
  console.log('middleware_1 end', 'test: ', cxt.test)
}
async function fn2(cxt) {
  console.log('middleware_2 start', 'test: ', cxt.test)
  cxt.test = 'middleware_2'
  await fn3(cxt)
  console.log('middleware_2 end', 'test: ', cxt.test)
}
async function fn3(cxt) {
  console.log('middleware_3 start', 'test: ', cxt.test)
  cxt.test = 'middleware_3'
  console.log('middleware_3 end', 'test: ', cxt.test)
}
fn1({})

// 输出
// middleware_1 start test:  undefined
// middleware_2 start test:  middleware_1
// middleware_3 start test:  middleware_2
// middleware_3 end test:  middleware_3
// middleware_2 end test:  middleware_3
// middleware_1 end test:  middleware_3
复制代码
分类:
前端
收藏成功!
已添加到「」, 点击更改