百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

「JavaScript」Nodejs之Koa源码解读

ccwgpt 2024-09-21 13:36 23 浏览 0 评论

简介

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

今天要简单的看一下koa的源码,首先需要说明的一点,本文都是笔者个人观点,同时我接触koa的时间并不长,设置接触nodejs的时间也不长,所以大家最好都带着批判的思维来阅读本文,希望不要对大家造成误导。

使用

最简单的一个应用

const Koa = require('koa');
const app = new Koa();
app.listen(3000);

这是一个无作用的 Koa 应用程序被绑定到 3000 端口

上手使用一个框架,只需要5分钟。正因为如此很多人都忽略了框架的内部实现。

知其然,知其所以然。我们下面了解一下其源码构成。

源码解析

进入正题,关于源码,找到了两套,一个在github上,https://github.com/koajs/koa

另一个是在我之前使用的koa项目里,项目里通过ide点击跳转过去的是一个 index.d.ts,是一个typescript的实现,不过这两套代码在功能上应该是完全一样的。

这里由于我之前并未接触过typescript,所以选择去解读一下GitHub上的开源代码。

在代码lib目录下,有4个文件application.js、context.js、request.js、response.js 差不多可以猜测结构,对外暴露的koa就是这边的 application.js 里export的对象,其他三个为application的重要组成部分。

先从application看起。

'use strict';
/**
 * Module dependencies.
 */
const isGeneratorFunction = require('is-generator-function');
const debug = require('debug')('koa:application');
const onFinished = require('on-finished');
const response = require('./response');
const compose = require('koa-compose');
const isJSON = require('koa-is-json');
const context = require('./context');
const request = require('./request');
const statuses = require('statuses');
const Emitter = require('events');
const util = require('util');
const Stream = require('stream');
const http = require('http');
const only = require('only');
const convert = require('koa-convert');
const deprecate = require('depd')('koa');

这边use strict开启JavaScript的严格模式,这种写法在框架代码中很常见,越是底层的代码,越需要严格,所以在我们平时写上层应用时,use strict就用的少很多了。

然后是依赖的相关文件,这里可以看到的确依赖同级目录下的context、request、response文件,但同时还依赖很多外部扩展。

  • is-generator-function 判断是否是generator函数
  • debug来调试,用于打印相关log
  • on-finished 看了说明,用来监听http请求结束时的回调
  • koa-compose用于构建中间件
  • koa-is-json用于Check if a body is JSON
  • statuses提供http的status单元
  • events实现了事件机制 Node's event emitter for all engines
  • util提供了一系列常用工具函数
  • stream 是 Node.js 中处理流式数据的抽象接口。 stream 模块提供了一些 API,用于构建实现了流接口的对象。Node.js 提供了多种流对象。 例如,HTTP 服务器的请求和 process.stdout 都是流的实例。流可以是可读的、可写的、或者可读可写的。 所有的流都是 EventEmitter 的实例。
  • http 要使用 HTTP 服务器与客户端,需要 require('http')
  • only Return whitelisted properties of an object.
  • koa-convert 用于支持之前版本的koajs
  • depd类似命名空间的用法

到这里就把依赖的库大致讲了一遍,当然没搞明白也没关系,这里只需要有个大致的概念,后面具体用到了可以回过头来在看。

然后就是application的主要部分了

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */
module.exports = class Application extends Emitter {
}

这里可以看到 application是继承自Emitter,也就是 events.继续看内部的代码

 /**
 * Initialize a new `Application`.
 *
 * @api public
 */
 constructor() {
 super();
 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);
 if (util.inspect.custom) {
 this[util.inspect.custom] = this.inspect;
 }
 }

这是构造函数,里面定义了一些成员变量,不一一说明了。继续往下到了listen方法

 /**
 * Shorthand for:
 *
 * http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */
 listen(...args) {
 debug('listen');
 const server = http.createServer(this.callback());
 return server.listen(...args);
 }

这里的listen其实就对http.createServer(callback).listen的一种缩写。算是一个语法糖。不难理解。继续往下。

/**
 * Return JSON representation.
 * We only bother showing settings.
 *
 * @return {Object}
 * @api public
 */
 toJSON() {
 return only(this, [
 'subdomainOffset',
 'proxy',
 'env'
 ]);
 }

/**
 * Inspect implementation.
 *
 * @return {Object}
 * @api public
 */
 inspect() {
 return this.toJSON();
 }

这个就是一个简单的成员变量保护,只对外暴露三个属性。

/**
 * Use the given middleware `fn`.
 *
 * Old-style middleware will be converted.
 *
 * @param {Function} fn
 * @return {Application} self
 * @api public
 */
 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;
 }

这个是use方法,平时的use就是把函数推到middleware属性中,如果是generator函数,这边会做个转化,然后再推入,逻辑也相当简单。

/**
 * 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;
 }

callback先把middleware组装好,然后开启了一个错误事件监听。然后有调用了下面的createContext和handleRequest方法。结合上面的listen方法可以看到callback被用到http.createServer(this.callback())中,也就是一个简单的封装而已。并不难理解,那继续往下看。

/**
 * 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);
 }

这个用在callback里的方法,可以看到执行了fnMiddleware方法,同时用到下面的onerror和respond。这里的onFinished也是一个监听吧,暂时这么理解,先往下看。

 /**
 * Initialize a new context.
 *
 * @api private
 */
 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.state = {};
 return context;
 }

这一步也不难理解,创建了一个context对象,并且初始化了他的各项属性值。接下来的onerror也很好理解。

/**
 * Default error handler.
 *
 * @param {Error} err
 * @api private
 */
 onerror(err) {
 if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
 if (404 == err.status || err.expose) return;
 if (this.silent) return;
 const msg = err.stack || err.toString();
 console.error();
 console.error(msg.replace(/^/gm, ' '));
 console.error();
 }

就在控制台打印了一些错误,并没什么难懂的。

然后application内部的方法就没了,梳理下来很少,并且几乎一眼看懂其作用,然后就是定义外的一个辅助方法,或者叫他工具方法。

/**
 * Response helper.
 */
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);
}

这个函数顾名思义,就是一个response的封装函数,50行代码,也很清晰。

总的看下来逻辑非常清楚,需要主要到一点的,所有的res和req都是同一个对象,在不同的方法里反复传递使用,所以非常方便。

看到这边,感觉koa的核心思想真的非常简单,说直接点仅仅就是实现了use而已,提供了一个干净的中间件框架。我们可以很方便的写中间件,除此之后,和原生的http模块几乎没什么区别。

看过很多框架源码、类库源码,koa可以说是我见过最简短的一个了。

希望大家能从我的分享中有所收获,还有不足不对之处欢迎指出。

相关推荐

迈向群体智能 | 智源发布首个跨本体具身大小脑协作框架

允中发自凹非寺量子位|公众号QbitAI3月29日,智源研究院在2025中关村论坛“未来人工智能先锋论坛”上发布首个跨本体具身大小脑协作框架RoboOS与开源具身大脑RoboBrain,可实...

大模型对接微信个人号,极空间部署AstrBot机器人,万事不求百度

「亲爱的粉丝朋友们好啊!今天熊猫又来介绍好玩有趣的Docker项目了,喜欢的记得点个关注哦!」引言前两天熊猫发过一篇关于如何在极空间部署AstrBot并对接QQ消息平台的文章,不过其实QQ现在已经很少...

Seata,让分布式事务不再是难题!实战分享带你领略Seata的魅力!

终身学习、乐于分享、共同成长!前言Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的...

常见分布式事务解决方案(分布式事务解决的问题)

1.两阶段提交(2PC)原理:分为准备阶段(协调者询问参与者是否可提交)和提交阶段(协调者根据参与者反馈决定提交或回滚)。优点:强一致性,适用于数据库层(如XA协议)。缺点:同步阻塞:所有参与者阻塞...

分布式事务:从崩溃到高可用,程序员必须掌握的实战方案!

“支付成功,但订单状态未更新!”、“库存扣减后,交易却回滚了!”——如果你在分布式系统中踩过这些“天坑”,这篇文章就是你的救命稻草!本文将手把手拆解分布式事务的核心痛点和6大主流解决方案,用代码实战+...

谈谈对分布式事务的一点理解和解决方案

分布式事务首先,做系统拆分的时候几乎都会遇到分布式事务的问题,一个仿真的案例如下:项目初期,由于用户体量不大,订单模块和钱包模块共库共应用(大war包时代),模块调用可以简化为本地事务操作,这样做只要...

一篇教你通过Seata解决分布式事务问题

1 Seata介绍Seata是由阿里中间件团队发起的开源分布式事务框架项目,依赖支持本地ACID事务的关系型数据库,可以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,目前提供AT...

Seata分布式事务详解(原理流程及4种模式)

Seata分布式事务是SpringCloudAlibaba的核心组件,也是构建分布式的基石,下面我就全面来详解Seata@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合...

分布式事务最终一致性解决方案有哪些?MQ、TCC、saga如何实现?

JTA方案适用于单体架构多数据源时实现分布式事务,但对于微服务间的分布式事务就无能为力了,我们需要使用其他的方案实现分布式事务。1、本地消息表本地消息表的核心思想是将分布式事务拆分成本地事务进行处理...

彻底掌握分布式事务2PC、3PC模型(分布式事务视频教程)

原文:https://mp.weixin.qq.com/s/_zhntxv07GEz9ktAKuj70Q作者:马龙台工作中使用最多的是本地事务,但是在对单一项目拆分为SOA、微服务之后,就会牵扯出分...

Seata分布式事务框架关于Annotation的SAGA模式分析

SAGAAnnotation是ApacheSeata版本2.3.0中引入的功能,它提供了一种使用Java注解而不是传统的JSON配置或编程API来实现SAGA事务模式的声明...

分布式事务,原理简单,写起来全是坑

今天我们就一起来看下另一种模式,XA模式!其实我觉得seata中的四种不同的分布式事务模式,学完AT、TCC以及XA就够了,Saga不好玩,而且长事务本身就有很多问题,也不推荐使用。S...

内存空间节约利器redis的bitmap(位图)应用场景有哪些你知道吗

在前面我们分享过一次Redis常用数据结构和使用场景,文章对Redis基本使用做了一个简单的API说明,但是对于其中String类型中的bitmap(位图)我们需要重点说明一下,因为他的作用真的不容忽...

分布式事务原理详解(图文全面总结)

分布式事务是非常核心的分布式系统,也是大厂经常考察对象,下面我就重点详解分布式事务及原理实现@mikechen本文作者:陈睿|mikechen文章来源:mikechen.cc分布式事务分布式事务指的是...

大家平时天天说的分布式系统到底是什么东西?

目录从单块系统说起团队越来越大,业务越来越复杂分布式出现:庞大系统分而治之分布式系统所带来的技术问题一句话总结:什么是分布式系统设计和开发经验补充说明:中间件系统及大数据系统前言现在有很多Java技术...

取消回复欢迎 发表评论: