前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Node学习笔记 - Koa源码阅读

Node学习笔记 - Koa源码阅读

作者头像
LamHo
发布2022-09-26 10:41:46
6010
发布2022-09-26 10:41:46
举报
文章被收录于专栏:小前端看世界小前端看世界

前言

最近经过一些反思,发现现在很多时候用node的框架,都缺乏对于node框架的源码理解和实现原理,所以会在接下来的一段时间里进行学习node的框架实现原理,从中去更加深入理解node当中的一些技巧以及一些细节上的问题。

现在经常用到node的项目是使用Egg来实现的,不得不说Egg是一个非常优秀的框架,而且Egg也是基于Koa来封装实现的,那么既然这样,我就打算先学习Koa的源码,以及好好看看Koa的使用,为以后自己造轮子做一个准备。

目的

看源码一定要带着目的去看,而不是漫无目的的为了看源码而看源码!Koa之所以被广大开发者认同,很重要的一点是它非常轻量级,轻得恐怖。基本就是对http模块的一些封装以及洋葱模型的思路。

整个源码阅读围绕着以下目的展开:

  • Koa是如何启动的
  • Koa如何封装req和res的
  • Koa的中间件原理和洋葱模型

Koa源码架构

一个如此受欢迎的框架,代码竟然如此之小!

  • application
  • context
  • request
  • response

只有4个文件,但是我现在还没有开始阅读,所以暂时并不知道这4个文件的作用,但是通过文件的命名可以知道,application是应用程序的入口文件,context应该是属于ctx范畴的一个文件,request和response应该是属于请求体和响应体的一些实现。

Koa启动流程

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

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

app.listen( 3000 );

一个非常简单的DEMO,我们去看启动的时候Koa帮我们做了什么?

首先我们require的koa实际上是application.js返回的一个class,我们的app就是通过这个class实例化出来的对象。

Application

  • proxy: 是否信任proxy header参数,默认为false
  • middleware: 保存通过app.use(middleware)注册的中间件
  • subdomainOffset: 保存通过app.use(middleware)注册的中间件
  • env: 环境参数,默认为NODE_ENV或'development'
  • context: context模块,通过context.js创建
  • request: request模块,通过request.js创建
  • response: response模块,通过response.js创建

实例化后的对象中有几个函数:

  1. listen
  2. toJSON
  3. inspect
  4. use
  5. callback
  6. handleRequest
  7. createContext
  8. onerror

但实际上我们会用到的就只有listen和use,其他函数很多都是内部自己调用。

listen

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

listen的实现很简单,实际上就是创建一个http服务,并且监听你传入的端口,这里的this.callback是重点!我们之后去看。

use

在Koa中,一切都是中间件,这个是它一个非常好的思想,有它的优势也有它的问题,我之后再去说。use这个api就是我们经常会用到的设置中间件的api,内部的代码实现也是很简单的。

代码语言:javascript
复制
use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
}

因为以前Koa1.x的时候并不是用await/async来实现洋葱模型的,所以需要使用isGeneratorFunction来做判断是用Generator还是用await/async来实现中间件,需要用convert这个库来进行兼容。当然我没有用过1.x版本和使用过Generator,所以不做过多了解,有await/async就可以了。

之前说到koa的class中有一个middleware变量,其实就是一个数组,在我们使用app.use的时候,实际上就是将函数push进middleware数组中,等待之后的调用。这个就是use的方法。实现的方式比较简单。

callback

callback这个函数是在我们调用listen函数的时候,内部createServer时传入的回调函数。

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

在这个callback中有一个非常重要的函数,compose函数,这个函数是来自koa-compose的,koa-compose就是实现洋葱模型的调用方式的关键所在。

其次,因为Koa的class是继承了Emitter的,所以在这里可以直接调用listenerCount来监听error事件,当发生了error的情况下,那么将会调用onerror函数来输出错误。

handleRequest函数就是将createServer返回的req和res放入createContext中创建出ctx上下文对象,并传入this.handleRequest中并返回this.handleRequest函数给createContext作为监听回调函数。

接下来我们会对以下几个函数进行详细阅读:

  1. koa-compose
  2. createContext
  3. handleRequest

koa-compose

koa-compose主要的作用就是将我们use进去的中间件数组转化为洋葱模式的执行方式的一个库。源码相对少,就是一个函数。

代码语言:javascript
复制
'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  // 是否为数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')

  // 循环判断数组中的item是否为函数
  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) // 返回第一个use的中间件函数
    // 调用的函数主体
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 获取当前传入下标的中间件函数
      let fn = middleware[i]

      // 防止最后一个中间件执行next进行无限循坏
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // 洋葱模型的触发方式,先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

如果不是太好理解的话,可以看下图,我尝试说得更简单。

因为每一个中间件都是一个async函数,所以我们调用await next()实际上是调用下一个中间件代码,当下一个中间代码执行完后,就回到上一个中间的next之后的代码继续执行,如此类推,从而实现出一个洋葱模型的中间件执行模式。

在上图可以看到,如果我们use了10个中间件,除非你在其中一个中间件不再调用next函数执行下一个中间件函数,否则,如果你有1万个中间,都会全部调用。这样的会带来一些性能问题。之后再koa-router中我们去详细看一下性能问题。

createContext

createContext实际上是对createServer中返回的req和res进行封装。

代码语言:javascript
复制
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);
    // 设置app,req,res,ctx在context,request,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互相引用
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
}

之前有说道,koa的源码架构中存在4个文件,其中这里就用到了context.js,request.js和response.js。通过Object.create的方式,创建一个基于context.js,request.js和response.js的新的context,request和response。从而实现每一次访问的ctx,req和res都是完全独立。

所以request和response是koa提供的,内置一些方法,req和res才是http模块中提供的原生对象。最终返回封装好的context到中间件去。

接下来我们来看看context,request和response里面分别做了什么事情。

context.js

  • inspect
  • toJSON - 获取当前ctx的内容
  • assert - http-assert,对http-errors的封装,一些基本的断言并设置http返回体
  • onerror - 手动触发error,并设置返回体。
  • cookies - 对cookies库的封装。ctx.cookies == new Cookies()

在context中有比较重要的一点,就是context使用了delegates这个库(tj大神的库)。主要是将context中的一些值和函数代理到request和response中,这样实际上我们调用ctx.hostname获取值的时候,实际上是调用了req.hostname。从而方便调用。

代码语言:javascript
复制
/**
 * 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');

request.js和response.js

request就不一一说明里面的内容,因为request里面基本上做个就只有2个时间,将request对象上的一些值代理到req上面,另外就是提供了一些额外的值和函数,基本上都是基于req上面的信息进行封装的。response也一样。

handleRequest

handleRequest就是提供给createServer的回调函数,接受组装好的ctx和中间件调用函数作为参数。

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

一开始就将res的statusCode定义为404。如果在我们没有设置body的情况下,默认就会返回404。当所有中间执行完毕,就会执行context中的respond函数。

代码语言:javascript
复制
function respond(ctx) {
  // 当ctx的respond为false可以绕过koa的兜底处理
  if (false === ctx.respond) return;
  // 当请求是scoket将根据socket的writable,否则都未true
  if (!ctx.writable) return;

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

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

  // 请求是HEAD的一些处理
  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      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);// 处理Buffer类型返回
  if ('string' == typeof body) return res.end(body);// 处理字符串类型返回
  if (body instanceof Stream) return body.pipe(res);// 处理Stream类型返回

  // body: json 对象处理,转为JSON字符串返回
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

返回code等于204、205、304,会主动将body清空

到这里基本上就是koa的源码阅读。koa源码中总体来说做了几件事情:

  1. 创建服务,监听端口
  2. 基于req,res封装出ctx
  3. 构建洋葱模型的中间件执行机制
  4. 对返回做统一处理
  5. 对ctx和全局的error做监听

之后会继续看koa中的路由机制是如何设计的。

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

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

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

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

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