前往小程序,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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
Swift进阶二:基本数据类型相关
而在Objective-C中,如果没有特殊的指明,我们所声明的都是变量。可以通过如下几种方式来声明常量:
拉维
2020/07/20
8830
Swift进阶二:基本数据类型相关
Swift 4.0 新特性
WWDC 2017 带来了很多惊喜,在这次大会上,Swift 4 也伴随着 Xcode 9 测试版来到了我们的面前,虽然正式版要8月底9月初才会公布,但很多强大的新特性正吸引我们去学习它。根据大会上已经开放的新特性,先一睹为快。 体验 Swift 4包含在Xcode 9中,您可以从Apple的开发者门户下载最新版本的Xcode 9(您必须拥有一个活跃的开发者帐户)。 每个Xcode测试版将在发布时捆绑最新的Swift 4快照。在阅读时,您会注意到[SE-xxxx]格式的链接。 这些链接将带您到相关的Swif
xiangzhihong
2018/02/06
1.8K0
Swift 4.0 新特性
Swift 5.2到5.4新特性整理
SE-0287提案改进了Swift使用隐式成员表达式的能力。Swift 5.4之后不但可以使用单个 使用,而且可以链起来使用。
小刀c
2022/08/16
2.3K0
Swift 5.2到5.4新特性整理
Why Swift? Generics(泛型), Collection(集合类型), POP(协议式编程), Memory Management(内存管理)
写这篇文章主要是为了给组内要做的分享准备内容。这段时间几个项目都用到 Swift,在上次 GIAC 大会上就被问到为什么要用 Swift,正好这个主题可以聊聊 Swift 的哪些特性吸引了我。
用户7451029
2020/06/16
1.2K0
Why Swift? Generics(泛型), Collection(集合类型), POP(协议式编程), Memory Management(内存管理)
swift4.0语法杂记(精简版)
一、swift简史 1、介绍 swift是苹果公司于2014年推出用于撰写OS和iOS应用程序的语言。它由苹果开发者工具部门总监“克里斯.拉特纳”在2010年开始着手设计,历时一年完成基本的架构。到后来苹果公司大力投入swift语言的研发,于2014年发布这一语言的第一版本。swift2.0之后的语法则趋于稳定,2017年发布的swift4.0虽有改动,但也只是增添了一些新特性。这些新特性需要在Xcode9上运行才能显示出效果。值得一提的是它支持unicode9,也就是说,可以用某些图片图标来充当变量。
谦谦君子修罗刀
2018/05/02
15.4K0
swift4.0语法杂记(精简版)
Swift学习笔记
这是一篇学习swift的笔记 Objective-C是很好的语言,Runtime机制、消息机制等也是爱不释手。 Swift一直在更新,闲暇时间学一遍。学习的Blog:《从零开始学swift》 以下代码全部在playground进行的尝试 变量 let 是常量 var 是变量 不能修改的使用常量可以提高程序的可读性。 var str = "Hello, playground" print(str) let constA:Int = 12 let constB = 12 let constC = 12
落影
2018/04/27
1.4K0
Swift学习笔记
Swift3.0 - 数据类型
// 插入操作 shoppingList.insert("Maple Syrup", at: 0)
酷走天涯
2018/09/14
6440
Swift 中风味各异的类型擦除
Swift的总体目标是强大得足以用于低级(low-level)系统编程,又足够容易以便初学者学习,有时会导致非常有趣的情况——当 Swift 功能强大的类型系统要求我们配置相当先进的技术来解决乍看之下似乎微不足道的问题的时候。
韦弦zhy
2021/04/19
1.7K0
Swift 中风味各异的类型擦除
Swift 3到5.1新特性整理
Swift 5.0 最重要的自然是ABI Stability, 对此可以看这篇 Swift ABI 稳定对我们到底意味着什么 。
小刀c
2022/08/16
4.8K0
Swift 3到5.1新特性整理
[golang][history]The Go Annotated Specification\ Go注释规范18c5b488a3b2e218c0e0cf2a7d4820d9da93a554
This document supersedes all previous Go spec attempts. The intent is to make this a reference for syntax and semantics. It is annotated with additional information not strictly belonging into a language spec.
landv
2021/01/29
7060
[golang][history]The Go Annotated Specification\ Go注释规范 266b9d49bfa3d2d16b4111378b1f9794373ee141
This document supersedes all previous Go spec attempts. The intent is to make this a reference for syntax and semantics. It is annotated with additional information not strictly belonging into a language spec.
landv
2021/01/29
6270
[golang][history]The Go Annotated Specification\ Go注释规范328df636c5f3e0875bc71a7eadf5a4a5084e0b13
This document supersedes all previous Go spec attempts. The intent is to make this a reference for syntax and semantics. It is annotated with additional information not strictly belonging into a language spec.
landv
2021/01/29
7230
标准库中的主要关联类型
SE-0346 已经引入了主要关联类型特性。本篇提议目的是为了在 Swift 标准库中使用此特性,为现有协议支持主要关联类型。此外,这篇提议还提供了一些通用的API设计建议,会对协议作者在添加对该特性的支持时提供便利。
DerekYuYi
2022/11/29
5120
在 Swift 中自定义操作符
很少有Swift功能能和使用自定义操作符的一样产生如此多的激烈辩论。虽然有些人发现它们真的有用,可以降低代码冗余,或实施轻量级语法扩展,但其他人认为应该完全避免它们。
韦弦zhy
2021/07/01
1.5K0
谈谈 Swift 中 Sequence(序列) 、Collection(集合) 和高阶函数
序列和集合是一门语言中重要的组成部分,下面我们就通过这篇文章来看看 Swift 中的序列和集合。
Swift社区
2021/11/26
2.2K0
谈谈 Swift 中 Sequence(序列) 、Collection(集合) 和高阶函数
Swift 5.6到5.10新特性整理
当你编写涉及共享状态的代码时,如果你不确保这个共享状态在跨线程使用时是安全的,你就会在许多地方遇到数据竞争的问题。
小刀c
2024/04/03
2.2K0
Swift 5.6到5.10新特性整理
如何在 Swift 中自定义操作符
很少有Swift功能能和使用自定义操作符的一样产生如此多的激烈辩论。虽然有些人发现它们真的有用,可以降低代码冗余,或实施轻量级语法扩展,但其他人认为应该完全避免它们。
Swift社区
2021/11/26
1.2K0
Go Quick Start 极简教程
IDE :使用 GoLand is a cross-platform IDE built specially for Go developers。 https://www.jetbrains.com/go/
一个会写诗的程序员
2021/05/06
7610
golang源码分析:go-reflect
使用反射的耗时是不使用的160倍左右,耗时主要分为三个部分:reflect.TypeOf(),reflect.New(),value.Field().Set(),如果我们尽量避免使用上述反射函数,或者替代上述函数是优化性能常常探索的方案。首先看下标准库里面TypeOf函数是怎么定义的:
golangLeetcode
2023/09/06
2670
golang源码分析:go-reflect
《Kotlin 极简教程 》第4章 基本数据类型与类型系统
到目前为止,我们已经了解了Kotlin的基本符号以及基础语法。我们可以看出,使用Kotlin写的代码更简洁、可读性更好、更富有生产力。
一个会写诗的程序员
2018/08/17
2.3K0
推荐阅读
相关推荐
Swift进阶二:基本数据类型相关
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文