Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Node学习笔记 - Koa源码阅读

Node学习笔记 - Koa源码阅读

作者头像
LamHo
发布于 2022-09-26 02:41:46
发布于 2022-09-26 02:41:46
64400
代码可运行
举报
文章被收录于专栏:小前端看世界小前端看世界
运行总次数:0
代码可运行

前言

最近经过一些反思,发现现在很多时候用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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

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

use

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
'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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
/**
 * 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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Dataset和DataLoader
Pytorch通常使用Dataset和DataLoader这两个工具类来构建数据管道。
lyhue1991
2020/07/20
2.4K0
Dataset和DataLoader
PyTorch中 Datasets & DataLoader 的介绍
用于处理数据样本的代码可能很快就会变得混乱且难以维护。理想情况下,为了获得更好的可读性和模块化,我们希望处理数据集的代码与模型训练代码分离。
JOYCE_Leo16
2024/04/16
2430
PyTorch中 Datasets & DataLoader 的介绍
Pytorch实现基于卷积神经网络的面部表情识别(详细步骤)「建议收藏」
另外,我整理了整个项目的精简版本,完整代码,开箱即用,教程详细,方便快捷!下载:Pytorch实现基于卷积神经网络的面部表情识别项目源码
全栈程序员站长
2022/08/29
1.3K0
Pytorch实现基于卷积神经网络的面部表情识别(详细步骤)「建议收藏」
【转载】PyTorch系列 (二): pytorch数据读取
本文首先介绍了有关预处理包的源码,接着介绍了在数据处理中的具体应用; 其主要目录如下:
marsggbo
2019/03/08
2.1K0
PyTorch 源码解读之 torch.utils.data:解析数据处理全流程
来源丨https://zhuanlan.zhihu.com/p/337850513
BBuf
2021/07/01
1.6K0
pytorch-DataLoader(数据迭代器)
本博客讲解了pytorch框架下DataLoader的多种用法,每一种方法都展示了实例,虽然有一点复杂,但是小伙伴静下心看一定能看懂哦 :)
全栈程序员站长
2022/07/01
1.3K0
PyTorch 小课堂开课啦!带你解析数据处理全流程(一)
最近被迫开始了居家办公,这不,每天认真工(mo)作(yu)之余,也有了更多时间重新学习分析起了 PyTorch 源码分享,属于是直接站在巨人的肩膀上了。在简单捋一捋思路之后,就从 torch.utils.data 数据处理模块开始,一步步重新学习 PyTorch 的一些源码模块解析,希望也能让大家重新认识已经不陌生的 PyTorch 这个小伙伴。
OpenMMLab 官方账号
2022/05/25
1.1K0
PyTorch 小课堂开课啦!带你解析数据处理全流程(一)
【深度学习】Pytorch 教程(十四):PyTorch数据结构:6、数据集(Dataset)与数据加载器(DataLoader):自定义鸢尾花数据类
  Tensor(张量)是PyTorch中用于表示多维数据的主要数据结构,类似于多维数组,可以存储和操作数字数据。
Qomolangma
2024/07/30
2010
【深度学习】Pytorch 教程(十四):PyTorch数据结构:6、数据集(Dataset)与数据加载器(DataLoader):自定义鸢尾花数据类
AI:使用pytorch通过BERT模型进行文本分类
BERT 是一个强大的语言模型,至少有两个原因:它使用从 BooksCorpus (有 8 亿字)和 Wikipedia(有 25 亿字)中提取的未标记数据进行预训练。它是通过利用编码器堆栈的双向特性进行预训练的。这意味着 BERT 不仅从左到右,而且从右到左从单词序列中学习信息。
Freedom123
2024/03/29
1.3K1
AI:使用pytorch通过BERT模型进行文本分类
小白学PyTorch | 8 实战之MNIST小试牛刀
在这个文章中,主要是来做一下MNIST手写数字集的分类任务。这是一个基础的、经典的分类任务。建议大家一定要跟着代码做一做,源码和数据已经上传到公众号。回复【pytorch】获取数据和源码哦~
机器学习炼丹术
2020/09/14
7910
小白学PyTorch | 8 实战之MNIST小试牛刀
小白学PyTorch | 3 浅谈Dataset和Dataloader
PyTorch 读取其他的数据,主要是通过 Dataset 类,所以先简单了解一下 Dataset 类。在看很多PyTorch的代码的时候,也会经常看到dataset这个东西的存在。Dataset类作为所有的 datasets 的基类存在,所有的 datasets 都需要继承它。
机器学习炼丹术
2020/09/03
2.4K0
小白学PyTorch | 3 浅谈Dataset和Dataloader
Pytorch划分数据集的方法
之前用过sklearn提供的划分数据集的函数,觉得超级方便。但是在使用TensorFlow和Pytorch的时候一直找不到类似的功能,之前搜索的关键字都是“pytorch split dataset”之类的,但是搜出来还是没有我想要的。结果今天见鬼了突然看见了这么一个函数torch.utils.data.Subset。我的天,为什么超级开心hhhh。终于不用每次都手动划分数据集了。 torch.utils.data Pytorch提供的对数据集进行操作的函数详见:https://pytorch.or
marsggbo
2019/05/26
4.5K0
【深度学习】Pytorch 教程(十五):PyTorch数据结构:7、模块(Module)详解(自定义神经网络模型并训练、评估)
  Tensor(张量)是PyTorch中用于表示多维数据的主要数据结构,类似于多维数组,可以存储和操作数字数据。
Qomolangma
2024/07/30
3990
【深度学习】Pytorch 教程(十五):PyTorch数据结构:7、模块(Module)详解(自定义神经网络模型并训练、评估)
pytorch笔记
pytorch刚上手确实不太容易适应。特别是Andrew给出的1.x的tensorflow代码,和当前torch的差异还是很大的。这里的用法挺琐碎的,用作备忘性质。
Sarlren
2022/10/28
2820
pytorch demo 实践
相关环境 python opencv pytorch ubuntu 14.04 pytorch 基本内容 60分钟快速入门,参考:https://blog.csdn.net/u014630987/article/details/78669051 需要学习的内容包括 1、基本概念Tensors、Variable、Numpy等 2、如何搭建神经网络模型(包括卷积神经网络) 3、如何定义损失函数和优化器(包括不同分类器和优化器的含义) 4、如何训练(包括如何读取数据、如何在GPU上
张俊怡
2018/04/24
2.1K0
pytorch demo 实践
时间序列数据建模流程范例
最开始在学习神经网络,PyTorch 的时候,懂的都还不多,虽然也知道 RNN, CNN 这些网络的原理,但真正自己实现起来又是另一回事,代码往往也都是从网上 copy 过来然后再自己魔改的,这也就导致了一系列的问题,代码格式不统一,没弄懂具体实现细节等等。当然,凭这些 copy 过来的代码让模型运行起来还是不难的,你只需要知晓一定的原理。显而易见,这些时间往往最后都是要“还”的。
EmoryHuang
2022/10/31
1.2K0
时间序列数据建模流程范例
【深度学习实验】前馈神经网络(七):批量加载数据(直接加载数据→定义类封装数据)
在本系列先前的代码中,借助深度学习框架的帮助,已经完成了前馈神经网络的大部分功能。本文将基于鸢尾花数据集构建一个数据迭代器,以便在每次迭代时从全部数据集中获取指定数量的数据。(借助深度学习框架中的Dataset类和DataLoader类来实现此功能)
Qomolangma
2024/07/30
1580
【深度学习实验】前馈神经网络(七):批量加载数据(直接加载数据→定义类封装数据)
基于PyTorch深度学习框架的序列图像数据装载器
如今,深度学习和机器学习算法正在统治世界。PyTorch是最常用的深度学习框架之一,用于实现各种深度学习算法。另一方面,基于学习的方法本质上需要一些带注释的训练数据集,这些数据集可以被模型用来提取输入数据和标签之间的关系。为了给神经网络提供数据,我们定义了一个数据加载器。
磐创AI
2021/09/03
6140
pytorch源码分析之torch.utils.data.Dataset类和torch.utils.data.DataLoader类
Pytorch深度学习框架优势之一是python优先,源代码由python代码层和C语言代码层组成,一般只需要理解python代码层就可以深入理解pytorch框架的计算原理。所以学习pytorch源码需要熟练掌握python语言的各种使用技巧。
全栈程序员站长
2022/08/24
1K0
pytorch源码分析之torch.utils.data.Dataset类和torch.utils.data.DataLoader类
Pytorch(五)入门:DataLoader 和 Dataset
构建模型的基本方法,我们了解了。 接下来,我们就要弄明白怎么对数据进行预处理,然后加载数据,我们以前手动加载数据的方式,在数据量小的时候,并没有太大问题,但是到了大数据量,我们需要使用 shuffle, 分割成mini-batch 等操作的时候,我们可以使用PyTorch的API快速地完成这些操作。
全栈程序员站长
2022/07/01
4440
Pytorch(五)入门:DataLoader 和 Dataset
推荐阅读
相关推荐
Dataset和DataLoader
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文