「JavaScript」Nodejs之Koa源码解读
ccwgpt 2024-09-21 13:36 33 浏览 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可以说是我见过最简短的一个了。
希望大家能从我的分享中有所收获,还有不足不对之处欢迎指出。
相关推荐
- 土豪农村建个别墅不新鲜 建个车库都用框架结构?
-
农村建房子过去都是没车库,也没有那么多豪车,一般直接停在路边或者院子里。现在很多人都会在建房子的时候留一个车库,通过车库可以直接进入客厅,省得雨雪天气折腾。农村土豪都是有钱任性,建房子跟我们普通人不一...
- 自建框架结构出现裂缝怎么回事?
-
三层自建房梁底与墙体连接处裂缝是结构问题吗?去前帮我姑画了一份三层自建房的图纸,前天他们全部装修好了。我姑丈突然打电话给我说他发现二层的梁底与墙分离了,有裂缝。也就是图纸中前面8.3米那跨梁与墙体衔接...
- 钢结构三维图集-框架结构(钢柱对接)
-
1、实腹式钢柱对接说明1:1.上节钢柱的安装吊点设置在钢柱的上部,利用四个吊点进行吊装;2.吊装前,下节钢柱顶面和本节钢柱底面的渣土和浮锈要清除干净,保证上下节钢柱对接面接触顶紧;3.钢柱吊装到位后...
- 三层框架结构主体自建房设计案例!布局13*12米占地面积156平米!
-
绘创意设计乡村好房子设计小编今日头条带来分享一款:三层框架结构主体自建房设计案例!布局13*12米占地面积156平米!本案例设计亮点:这是一款三层新中式框架结构自建房,占地13×12米,户型占地面积...
- 农村自建房新宠!半框架结构凭啥这么火?内行人揭开3个扎心真相
-
回老家闲逛,竟发现个有意思的现象:村里盖新房,十家有八家都选了"半框架结构"。隔壁王叔家那栋刚封顶的二层小楼,外墙红砖还露着糙面没勾缝,里头的水泥柱子倒先支棱得笔直,这到底是啥讲究?蹲...
- 砖混结构与框架结构!究竟有何区别?千万别被坑!
-
农村自建房选结构,砖混省钱但出事真能保命吗?7月建材价格波动期,多地建房户因安全焦虑陷入选择困境——框架结构虽贵30%,却是地震区保命的关键。框架柱和梁组成的承重体系,受力分散得像一张网。砖混靠墙硬扛...
- 砖混结构与框架结构,究竟有何区别?千万别被坑!
-
农村建房选砖混结构还是框架结构?这个问题算是近期留言板里问得最多的问题了。今天咱们说说二者的区别,帮您选个合适的。01成本区别假如盖一栋砖混结构的房子需要30万,那么换成框架结构,一般要多掏30%的费...
- 6个小众却逆天的App神器,个个都是黑科技的代表
-
你的手机上有哪些好用的软件?今天我就给大家分享6个小众却逆天的App神器,个个都是黑科技的代表!01*Via浏览器推荐理由:体积极小的浏览器,没有任何广告。使用感受:它的体量真的很小,只有702KB,...
- 合肥App开发做一个app需要多少钱?制作周期有多久?
-
在移动互联网时代,开发一款APP已成为企业数字化转型与个人创业的重要途径。然而,APP的开发成本与制作周期受功能复杂度、技术架构、团队类型等多重因素影响,差异极大。好牛软件将从这两个维度展开分析,帮助...
- 详解应对App臃肿化的五大法则
-
编者注:本文转自腾讯ISUX。先来看一张图:图上看到,所有平台上用户花费时间都在减少,除了移动端。观察身边也是如此,回家不开电脑的小伙伴越来越多。手机平板加电视,下班场景全搞定。连那些以前电脑苦手的...
- 实战!如何从零搭建10万级 QPS 大流量、高并发优惠券系统
-
需求背景春节活动中,多个业务方都有发放优惠券的需求,且对发券的QPS量级有明确的需求。所有的优惠券发放、核销、查询都需要一个新系统来承载。因此,我们需要设计、开发一个能够支持十万级QPS的券系...
- 8种移动APP导航设计模式大对比
-
当我们确定了移动APP的设计需求和APP产品设计流程之后,开始着手设计APP界面UI或是APP原型图啦。这个时候我们都要面临的第一个问题就是如何将信息以最优的方式组合起来?也许我们对比和了解了其他一些...
- 数字资产支付 App 的技术框架
-
开发一款功能强大、安全可靠的数字资产支付App需要一个整合了区块链技术、后端服务、前端应用以及第三方集成的全栈技术框架。这个框架的核心在于保障数字资产的安全流通,并将其高效地桥接到传统的法币支付场...
- 从MyBatis到App架构:设计模式全景应用指南
-
从MyBatis到App架构:设计模式全景应用指南引言在企业级应用和服务端开发领域,MyBatis凭借其灵活、简洁、强大的ORM映射能力被广泛应用。而它之所以能拥有如此优秀的可扩展性和工程可维护性,正...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- mfc框架 (52)
- abb框架断路器 (48)
- beego框架 (52)
- java框架spring (58)
- grpc框架 (65)
- tornado框架 (48)
- 前端框架bootstrap (54)
- orm框架有哪些 (51)
- 知识框架图 (52)
- ppt框架 (55)
- 内联框架 (52)
- cad怎么画框架 (58)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)