前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【koa快速入门】之深究原理

【koa快速入门】之深究原理

作者头像
luciozhang
发布2023-04-22 16:31:09
2330
发布2023-04-22 16:31:09
举报
文章被收录于专栏:前端lucio前端lucio

前言

koa是Express团队打造的新一代web框架,特点是更小,更舒服的开发体验。

前两节我们已经介绍了koa的基本使用和koa项目的最佳实践,今天我们来深究下koa2的原理。

《koa2教程》思维导图
《koa2教程》思维导图

初看源码

查看koa2的源码,可以发现其实现代码非常简单,只有四个js文件。

下面先从这四个js文件介绍源码的大概结构:

application.js

是koa2的入口文件,在当中有Koa实例的构造函数,该构造函数继承events,来实现对(错误)事件的触发和监听。

listen函数,是对http.createServer的封装。

代码语言:javascript
复制
listen (...args) {
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
}

use函数,收集中间件。

代码语言:javascript
复制
use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this
}

callback函数,是用于处理中间件,安排中间件的执行顺序,并返回http.createServer可以处理的回调函数。

代码语言:javascript
复制
  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */  

  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
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

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

context.js

就是中间件参数中的ctx,上下文,最主要的功能是基于delegates模块实现的。

delegates 基本用法就是将内部对象的变量或者函数绑定在暴露在外层的变量上。

通过delegates把ctx.repsponse.status等等repsponse和request上的属性,暴露在ctx上,类似ctx.status。

代码语言:javascript
复制
const proto = module.exports = {
  //...
}

delegate(proto, 'response')
  .method('attachment')
  .access('status')
//...

request.js和response.js

这两个类是对原生req和res的封装(这个原生req和res是http.createServer的回调函数返回的),用get和set对外暴露了很多方便使用的属性和方法,我们ctx访问的repsponse和request上的属性,其实是这些get和set方法。

代码语言:javascript
复制
  /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */

  get status () {
    return this.res.statusCode
  },

  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status (code) {
    if (this.headerSent) return

    assert(Number.isInteger(code), 'status code must be a number')
    assert(code >= 100 &amp;&amp; code <= 999, `invalid status code: ${code}`)
    this._explicitStatus = true
    this.res.statusCode = code
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
    if (this.body &amp;&amp; statuses.empty[code]) this.body = null
  },

深究原理

中间件和洋葱模型

下面重点介绍中间件洋葱模型执行顺序实现。

首先我们要了解中间件的执行顺序,先看下面这段代码。

代码语言:javascript
复制
let Koa = require('koa');

let app = new Koa();

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

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

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});

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

输出的顺序是123456,koa的中间件是按洋葱模型的顺序执行的。

中间件之间通过 next 函数联系,当一个中间件调用 next() 后,会将控制权交给下一个中间件,直到下一个中间件不再执行 next() 时沿路返回,依次将控制权交给上一个中间件。

vqrjv4796k
vqrjv4796k

那么,怎么实现这种执行顺序呢?

上面初看代码的时候,我们已经知道,在use中通过this.middleware.push(fn)完成了中间件的搜集,然后在callback中处理中间件的执行顺序。

我们先回顾一下koa的application.js。

代码语言:javascript
复制
 /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */  

  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
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

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

再看一下koa-compose中compose函数的实现。

代码语言:javascript
复制
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!')
  }

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

我们先删掉不重要的部分,整理下代码,只保留实现洋葱模型执行顺序的代码。

代码语言:javascript
复制
function compose (middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch (i) {
      let fn = middleware[i]
      if (i === middleware.length) fn = next  // 最后一个中间件也处理完,fn指向next
      if (!fn) {
        return Promise.resolve() // fn为null直接resolve
      } else {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      }
    }
  }
}

重点在fn(context, dispatch.bind(null, i + 1)),在resolve中,调用了fn,在fn中又调用了dispatch(i+1)

我们可以先理解fn实现了下面这样的接口。

代码语言:javascript
复制
function fn(ctx, next){
    return next()
}

用前面打印顺序的例子,我们来盘一下执行顺序。

  1. dispatch(0),fn指向第一个中间件,在resolve中执行,然后就 console.log(1);
  2. 然后遇到了next(),于是调用到dispatch(1),fn指向第二个中间件,执行fn,console.log(2)
  3. 然后遇到了next(),于是调用到dispatch(2),fn指向第三个中间件,执行fn,console.log(3),继续console.log(4)
  4. 然后,第二个中间件中的next执行完返回了,就继续执行第二个中间件next后面的console.log(5)。(函数调用栈的原理)
  5. 然后,第一个中间件中的next执行完返回了,就继续执行第一个中间件next后面的console.log(6)

OK了,顺序这就搞清楚了!

总结

了解了koa2的源码,最直观的一个感受就是,koa2的实现方式很先进,而且很简洁。

大量使用了es6的新特性,和一些功能强大又小巧的第三方模块,最终的koa2的产品,也遵从这种简洁的设计理念,只做好一个中间件框架,不附带一点点其他更多的功能。

参考文献

koa官网

KOA2框架原理解析和实现

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-04-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 初看源码
    • application.js
      • context.js
        • request.js和response.js
        • 深究原理
          • 中间件和洋葱模型
          • 总结
          • 参考文献
          相关产品与服务
          消息队列 TDMQ
          消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档