前言
你好,我是若川。这是
学习源码整体架构系列第七篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。
学习源码整体架构系列文章如下:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
感兴趣的读者可以点击阅读。
其他源码计划中的有:express、vue-router、react-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。
源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。
如果你简历上一不小心写了熟悉koa,面试官大概率会问:
1、
koa洋葱模型怎么实现的。2、如果中间件中的
next()方法报错了怎么办。3、
co的原理是怎样的。等等问题
导读
文章通过例子调试koa,梳理koa的主流程,来理解koa-compose洋葱模型原理和co库的原理,相信看完一定会有所收获。
本文目录
本文学习的koa版本是v2.11.0。克隆的官方仓库的master分支。
截至目前(2020年3月11日),最新一次commit是2020-01-04 07:41 Olle Jonsson eda27608,build: Drop unused Travis sudo: false directive (#1416)。
本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis。求个star呀。
本文阅读最佳方式
先star一下我的仓库,再把它git clone https://github.com/lxchuan12/koa-analysis.git克隆下来。不用管你是否用过nodejs。会一点点promise、generator、async、await等知识即可看懂。如果一点点也不会,可以边看阮一峰老师的《ES6标准入门》相关章节。跟着文章节奏调试和示例代码调试,动手调试(用vscode或者chrome)印象更加深刻。文章长段代码不用细看,可以调试时再细看。看这类源码文章百遍,可能不如自己多调试几遍。也欢迎加我微信交流lxchuan12。
1 | 复制代码# 克隆我的这个仓库 |
这里把这个examples文件夹做个简单介绍。
middleware文件夹是用来vscode调试整体流程的。simpleKoa文件夹是koa简化版,为了调试koa-compose洋葱模型如何串联起来各个中间件的。koa-convert文件夹是用来调试koa-convert和co源码的。co-generator文件夹是模拟实现co的示例代码。
vscode 调试 koa 源码方法
之前,我在知乎回答了一个问题一年内的前端看不懂前端框架源码怎么办?
推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:
1.借助调试
2.搜索查阅相关高赞文章
3.把不懂的地方记录下来,查阅相关文档
4.总结
看源码,调试很重要,所以我详细写下 koa 源码调试方法,帮助一些可能不知道如何调试的读者。
1 | 复制代码# 我已经克隆到我的koa-analysis仓库了 |
1 | 复制代码// package.json |
克隆源码后,看package.json找到main,就知道入口文件是lib/application.js了。
大概看完项目结构后发现没有examples文件夹(一般项目都会有这个文件夹,告知用户如何使用该项目),这时仔细看README.md。
如果看英文README.md有些吃力,会发现在Community标题下有一个中文文档 v2.x。同时也有一个examples仓库。
1 | 复制代码# 我已经克隆下来到我的仓库了 |
这时再开心的把examples克隆到自己电脑。可以安装好依赖,逐个研究学习下这里的例子,然后可能就一不小心掌握了koa的基本用法。当然,我这里不详细写这一块了,我是自己手写一些例子来调试。
继续看文档会发现使用指南讲述编写中间件。
使用文档中的中间件koa-compose例子来调试
学习 koa-compose 前,先看两张图。
洋葱模型示意图
洋葱模型中间件示意图
在koa中,请求响应都放在中间件的第一个参数context对象中了。
再引用Koa中文文档中的一段:
如果您是前端开发人员,您可以将 next(); 之前的任意代码视为“捕获”阶段,这个简易的 gif 说明了 async 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:
中间件gif图
- 创建一个跟踪响应时间的日期
- 等待下一个中间件的控制
- 创建另一个日期跟踪持续时间
- 等待下一个中间件的控制
- 将响应主体设置为“Hello World”
- 计算持续时间
- 输出日志行
- 计算响应时间
- 设置
X-Response-Time头字段- 交给 Koa 处理响应
读者们看完这个gif图,也可以思考下如何实现的。根据表现,可以猜测是next是一个函数,而且返回的可能是一个promise,被await调用。
看到这个gif图,我把之前写的examples/koa-compose的调试方法含泪删除了。默默写上gif图上的这些代码,想着这个读者们更容易读懂。
我把这段代码写在这里 koa/examples/middleware/app.js便于调试。
在项目路径下配置新建.vscode/launch.json文件,program配置为自己写的koa/examples/middleware/app.js文件。
.vscode/launch.json 代码,点击这里展开/收缩,可以复制
1 | 复制代码{ |
按F5键开始调试,调试时先走主流程,必要的地方打上断点,不用一开始就关心细枝末节。
断点调试要领:
赋值语句可以一步跳过,看返回值即可,后续详细再看。
函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。
上述比较啰嗦的写了一堆调试方法。主要是想着授人予鱼不如授人予渔,这样换成其他源码也会调试了。
简单说下chrome调试nodejs,chrome浏览器打开chrome://inspect,点击配置**configure…**配置127.0.0.1:端口号(端口号在Vscode 调试控制台显示了)。
更多可以查看English Debugging Guide
喜欢看视频的读者也可以看慕课网这个视频node.js调试入门,讲得还是比较详细的。
不过我感觉在chrome调试nodejs项目体验不是很好(可能是我方式不对),所以我大部分具体的代码时都放在html文件script形式,在chrome调试了。
先看看 new Koa() 结果app是什么
看源码我习惯性看它的实例对象结构,一般所有属性和方法都放在实例对象上了,而且会通过原型链查找形式查找最顶端的属性和方法。
用koa/examples/middleware/app.js文件调试时,先看下执行new Koa()之后,app是什么,有个初步印象。
1 | 复制代码// 文件 koa/examples/middleware/app.js |
在调试控制台ctrl + 反引号键(一般在Tab上方的按键)唤起,输入app,按enter键打印app。会有一张这样的图。
koa 实例对象调试图
VScode也有一个代码调试神器插件Debug Visualizer。
安装好后插件后,按ctrl + shift + p,输入Open a new Debug Visualizer View,来使用,输入app,显示是这样的。
koa 实例对象可视化简版
不过目前体验来看,相对还比较鸡肋,只能显示一级,而且只能显示对象,相信以后会更好。更多玩法可以查看它的文档。
我把koa实例对象比较完整的用xmind画出来了,大概看看就好,有个初步印象。
koa 实例对象
接着,我们可以看下app 实例、context、request、request的官方文档。
app 实例、context、request、request 官方API文档
可以真正使用的时候再去仔细看文档。
koa 主流程梳理简化
通过F5启动调试(直接跳到下一个断点处)、F10单步跳过、F11单步调试等,配合重要的地方断点,调试完整体代码,其实比较容易整理出如下主流程的代码。
1 | 复制代码class Emitter{ |
重点就在listen函数里的compose这个函数,接下来我们就详细来欣赏下这个函数。
koa-compose 源码(洋葱模型实现)
通过app.use() 添加了若干函数,但是要把它们串起来执行呀。像上文的gif图一样。
compose函数,传入一个数组,返回一个函数。对入参是不是数组和校验数组每一项是不是函数。
1 | 复制代码function compose (middleware) { |
把简化的代码和koa-compose代码写在了一个文件中。koa/examples/simpleKoa/koa-compose.js
1 | 复制代码hs koa/examples/ |
不过这样好像还是有点麻烦,我还把这些代码放在codepen https://codepen.io/lxchuan12/pen/wvarPEb中,直接可以在线调试啦。是不是觉得很贴心^_^,自己多调试几遍便于消化理解。
你会发现compose就是类似这样的结构(移除一些判断)。
1 | 复制代码// 这样就可能更好理解了。 |
也就是说
koa-compose返回的是一个Promise,Promise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。第一个
next函数里也是返回的是一个Promise,Promise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。第二个
next函数里也是返回的是一个Promise,Promise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。第三个…
以此类推。最后一个中间件中有调用
next函数,则返回Promise.resolve。如果没有,则不执行next函数。
这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。
不得不说非常惊艳,“玩还是大神会玩”。
这种把函数存储下来的方式,在很多源码中都有看到。比如lodash源码的惰性求值,vuex也是把action等函数存储下,最后才去调用。
搞懂了koa-compose 洋葱模型实现的代码,其他代码就不在话下了。
错误处理
仔细看文档,文档中写了三种捕获错误的方式。
ctx.onerror中间件中的错误捕获app.on('error', (err) => {})最外层实例事件监听形式
也可以看看例子koajs/examples/errors/app.js 文件app.onerror = (err) => {}重写onerror自定义形式
也可以看测试用例 onerror
1 | 复制代码// application.js 文件 |
ctx.onerror
lib/context.js文件中,有一个函数onerror,而且有这么一行代码this.app.emit('error', err, this)。
1 | 复制代码module.exports = { |
1 | 复制代码app.use(async (ctx, next) => { |
try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);,这里的onerror是ctx.onerror
而ctx.onerror函数中又调用了this.app.emit('error', err, this),所以在最外围app.on('error',err => {})可以捕获中间件链中的错误。
因为koa继承自events模块,所以有’emit’和on等方法)
koa2 和 koa1 的简单对比
koa1中主要是generator函数。koa2中会自动转换generator函数。
1 | 复制代码// Koa 将转换 |
koa-convert 源码
在vscode/launch.json文件,找到这个program字段,修改为"program": "${workspaceFolder}/koa/examples/koa-convert/app.js"。
通过F5启动调试(直接跳到下一个断点处)、F10单步跳过、F11单步调试调试走一遍流程。重要地方断点调试。
app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。
1 | 复制代码class Koa extends Emitter{ |
koa-convert源码挺多,核心代码其实是这样的。
1 | 复制代码function convert(){ |
最后还是通过co来转换的。所以接下来看co的源码。
co 源码
本小节的示例代码都在这个文件夹koa/examples/co-generator中,hs koa/example,可以自行打开https://localhost:8080/co-generator调试查看。
看co源码前,先看几段简单代码。
1 | 复制代码// 写一个请求简版请求 |
1 | 复制代码// 获取generator的值 |
简单来说co,就是把generator自动执行,再返回一个promise。generator函数这玩意它不自动执行呀,还要一步步调用next(),也就是叫它走一步才走一步。
所以有了async、await函数。
1 | 复制代码// await 函数 自动执行 |
也就是说co需要做的事情,是让generator向async、await函数一样自动执行。
模拟实现简版 co(第一版)
这时,我们来模拟实现第一版的co。根据generator的特性,其实容易写出如下代码。
1 | 复制代码// 获取generator的值 |
模拟实现简版 co(第二版)
但是实际上,不会上面那么简单的。有可能是多个yield和传参数的情况。
传参可以通过这如下两行代码来解决。
1 | 复制代码const args = Array.prototype.slice.call(arguments, 1); |
两个yield,我大不了重新调用一下promise.then,搞定。
1 | 复制代码// 多个yeild,传参情况 |
模拟实现简版 co(第三版)
问题是肯定不止两次,无限次的yield的呢,这时肯定要把重复的封装起来。而且返回是promise,这就实现了如下版本的代码。
1 | 复制代码function* generatorFunc(suffix = ''){ |
但第三版的模拟实现简版co中,还没有考虑报错和一些参数合法的情况。
最终来看下co源码
这时来看看co的源码,报错和错误的情况,错误时调用reject,是不是就好理解了一些呢。
1 | 复制代码function co(gen) { |
koa 和 express 简单对比
文档里写的挺全面的。简单来说koa2语法更先进,更容易深度定制(egg.js、think.js、底层框架都是koa)。
总结
文章通过授人予鱼不如授人予鱼的方式,告知如何调试源码,看完了koa-compose洋葱模型实现,koa-convert和co等源码。
koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise。
koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。
co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。
koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。
本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis。求个star呀。
1 | 复制代码git clone https://github.com/lxchuan12/koa-analysis.git |
再强烈建议下按照本文阅读最佳方式,克隆代码下来,动手调试代码学习更加深刻。
如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出,也欢迎加我微信交流
lxchuan12。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。
解答下开头的提问
仅供参考
1、
koa洋葱模型怎么实现的。
可以参考上文整理的简版koa-compose作答。
1 | 复制代码// 这样就可能更好理解了。 |
答:app.use() 把中间件函数存储在middleware数组中,最终会调用koa-compose导出的函数compose返回一个promise,中间函数的第一个参数ctx是包含响应和请求的一个对象,会不断传递给下一个中间件。next是一个函数,返回的是一个promise。
2、如果中间件中的
next()方法报错了怎么办。
可参考上文整理的错误处理作答。
1 | 复制代码ctx.onerror = function { |
答:中间件链错误会由ctx.onerror捕获,该函数中会调用this.app.emit('error', err, this)(因为koa继承自events模块,所以有’emit’和on等方法),可以使用app.on('error', (err) => {}),或者app.onerror = (err) => {}进行捕获。
3、
co的原理是怎样的。答:
co的原理是通过不断调用generator函数的next方法来达到自动执行generator函数的,类似async、await函数自动执行。
答完,面试官可能觉得小伙子还是蛮懂koa的啊。当然也可能继续追问,直到答不出…
还能做些什么 ?
学完了整体流程,koa-compose、koa-convert和co的源码。
还能仔细看看看http请求上下文(context)、http请求对象、http响应对象的具体实现。
还能根据我文章说的调试方式调试koa 组织中的各种中间件,比如koa-bodyparser, koa-router,koa-jwt,koa-session、koa-cors等等。
还能把examples仓库克隆下来,我的这个仓库已经克隆了,挨个调试学习下源码。
web框架有很多,比如Express.js,Koa.js、Egg.js、Nest.js、Next.js、Fastify.js、Hapi.js、Restify.js、Loopback.io、Sails.js、Midway.js等等。
还能把这些框架的优势劣势、设计思想等学习下。
还能继续学习HTTP协议、TCP/IP协议网络相关,虽然不属于koa的知识,但需深入学习掌握。
学无止境~
推荐阅读
koa 官网 | koa 仓库 | koa 组织 | koa2 中文文档 | co 仓库
深入浅出vue.js 作者 berwin: 深入浅出 Koa2 原理
另一个系列
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
若川的博客,使用vuepress重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
语雀前端视野专栏,新增语雀专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star^_^~
欢迎加微信交流 微信公众号
可能比较有趣的微信公众号,长按扫码关注。欢迎加我微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~
若川视野
本文转载自: 掘金