学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

  1. 创建一个跟踪响应时间的日期
  1. 等待下一个中间件的控制
  1. 创建另一个日期跟踪持续时间
  1. 等待下一个中间件的控制
  1. 将响应主体设置为“Hello World”
  1. 计算持续时间
  1. 输出日志行
  1. 计算响应时间
  1. 设置 X-Response-Time 头字段
  1. 交给 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 代码,点击这里展开/收缩,可以复制

F5键开始调试,调试时先走主流程,必要的地方打上断点,不用一开始就关心细枝末节。

断点调试要领:

赋值语句可以一步跳过,看返回值即可,后续详细再看。

函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。

上述比较啰嗦的写了一堆调试方法。主要是想着授人予鱼不如授人予渔,这样换成其他源码也会调试了。

简单说下chrome调试nodejschrome浏览器打开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是什么,有个初步印象。

// 文件 koa/examples/middleware/app.js

const Koa = require(‘…/…/lib/application’);

// const Koa = require(‘koa’);

// 这里打个断点

const app = new Koa();

// x-response-time

// 这里打个断点

app.use(async (ctx, next) => {

});

在调试控制台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文档

  • index API | context API | request API | response API

可以真正使用的时候再去仔细看文档。

koa 主流程梳理简化


通过F5启动调试(直接跳到下一个断点处)F10单步跳过F11单步调试等,配合重要的地方断点,调试完整体代码,其实比较容易整理出如下主流程的代码。

class Emitter{

// node 内置模块

constructor(){

}

}

class Koa extends Emitter{

constructor(options){

super();

options = options || {};

this.middleware = [];

this.context = {

method: ‘GET’,

url: ‘/url’,

body: undefined,

set: function(key, val){

console.log(‘context.set’, key, val);

},

};

}

use(fn){

this.middleware.push(fn);

return this;

}

listen(){

const fnMiddleware = compose(this.middleware);

const ctx = this.context;

const handleResponse = () => respond(ctx);

const onerror = function(){

console.log(‘onerror’);

};

fnMiddleware(ctx).then(handleResponse).catch(onerror);

}

}

function respond(ctx){

console.log(‘handleResponse’);

console.log(‘response.end’, ctx.body);

}

重点就在listen函数里的compose这个函数,接下来我们就详细来欣赏下这个函数。

koa-compose 源码(洋葱模型实现)


通过app.use() 添加了若干函数,但是要把它们串起来执行呀。像上文的gif图一样。

compose函数,传入一个数组,返回一个函数。对入参是不是数组和校验数组每一项是不是函数。

function compose (middleware) {

if (!Array.isArray(middleware)) throw new TypeError(‘Middleware stack must be an array!’)

for (const fn of middleware) {

if (typeof fn !== ‘function’) throw new TypeError(‘Middleware must be composed of functions!’)

}

// 传入对象 context 返回Promise

return function (context, next) {

// last called middleware #

let index = -1

return dispatch(0)

function dispatch (i) {

if (i <= index) return Promise.reject(new Error(‘next() called multiple times’))

index = i

let fn = middleware[i]

if (i === middleware.length) fn = next

if (!fn) return Promise.resolve()

try {

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

} catch (err) {

return Promise.reject(err)

}

}

}

}

把简化的代码和koa-compose代码写在了一个文件中。koa/examples/simpleKoa/koa-compose.js

hs koa/examples/

然后可以打开localhost:8080/simpleKoa,开心的把代码调试起来

不过这样好像还是有点麻烦,我还把这些代码放在codepenhttps://codepen.io/lxchuan12/pen/wvarPEb中,直接可以在线调试啦。是不是觉得很贴心_,自己多调试几遍便于消化理解。

你会发现compose就是类似这样的结构(移除一些判断)。

// 这样就可能更好理解了。

// simpleKoaCompose

const [fn1, fn2, fn3] = this.middleware;

const fnMiddleware = function(context){

return Promise.resolve(

fn1(context, function next(){

return Promise.resolve(

fn2(context, function next(){

return Promise.resolve(

fn3(context, function next(){

return Promise.resolve();

})

)

})

)

})

);

};

fnMiddleware(ctx).then(handleResponse).catch(onerror);

也就是说koa-compose返回的是一个PromisePromise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。

第一个next函数里也是返回的是一个PromisePromise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。

第二个next函数里也是返回的是一个PromisePromise中取出第三个函数(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

// application.js 文件

class Application extends Emitter {

// 代码有简化组合

listen(){

const fnMiddleware = compose(this.middleware);

if (!this.listenerCount(‘error’)) this.on(‘error’, this.onerror);

const onerror = err => ctx.onerror(err);

fnMiddleware(ctx).then(handleResponse).catch(onerror);

}

onerror(err) {

// 代码省略

// …

}

}

ctx.onerror

lib/context.js文件中,有一个函数onerror,而且有这么一行代码this.app.emit('error', err, this)

module.exports = {

onerror(){

// delegate

// app 是在new Koa() 实例

this.app.emit(‘error’, err, this);

}

}

app.use(async (ctx, next) => {

try {

await next();

} catch (err) {

err.status = err.statusCode || err.status || 500;

throw err;

}

});

try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);,这里的onerrorctx.onerror

ctx.onerror函数中又调用了this.app.emit('error', err, this),所以在最外围app.on('error',err => {})可以捕获中间件链中的错误。因为koa继承自events模块,所以有’emit’和on等方法)

koa2 和 koa1 的简单对比


中文文档中描述了 koa2 和 koa1 的区别

koa1中主要是generator函数。koa2中会自动转换generator函数。

// Koa 将转换

app.use(function *(next) {

const start = Date.now();

yield next;

const ms = Date.now() - start;

console.log(${this.method} ${this.url} - ${ms}ms);

});

koa-convert 源码

vscode/launch.json文件,找到这个program字段,修改为"program": "${workspaceFolder}/koa/examples/koa-convert/app.js"

通过F5启动调试(直接跳到下一个断点处)F10单步跳过F11单步调试调试走一遍流程。重要地方断点调试。

app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

class Koa extends Emitter{

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;

}

}

koa-convert源码挺多,核心代码其实是这样的。

function convert(){

return function (ctx, next) {

return co.call(ctx, mw.call(ctx, createGenerator(next)))

}

function * createGenerator (next) {

return yield next()

}

}

最后还是通过co来转换的。所以接下来看co的源码。

co 源码

tj大神写的co 仓库

本小节的示例代码都在这个文件夹koa/examples/co-generator中,hs koa/example,可以自行打开https://localhost:8080/co-generator调试查看。

co源码前,先看几段简单代码。

// 写一个请求简版请求

function request(ms= 1000) {

return new Promise((resolve) => {

setTimeout(() => {

resolve({name: ‘若川’});

}, ms);

});

}

// 获取generator的值

function* generatorFunc(){

const res = yield request();

console.log(res, ‘generatorFunc-res’);

}

generatorFunc(); // 报告,我不会输出你想要的结果的

简单来说co,就是把generator自动执行,再返回一个promisegenerator函数这玩意它不自动执行呀,还要一步步调用next(),也就是叫它走一步才走一步

所以有了async、await函数。

// await 函数 自动执行

async function asyncFunc(){

const res = await request();

console.log(res, ‘asyncFunc-res await 函数 自动执行’);

}

asyncFunc(); // 输出结果

也就是说co需要做的事情,是让generatorasync、await函数一样自动执行。

模拟实现简版 co(第一版)

这时,我们来模拟实现第一版的co。根据generator的特性,其实容易写出如下代码。

// 获取generator的值

function* generatorFunc(){

const res = yield request();

console.log(res, ‘generatorFunc-res’);

}

function coSimple(gen){

gen = gen();

console.log(gen, ‘gen’);

const ret = gen.next();

const promise = ret.value;

promise.then(res => {

gen.next(res);

});

}

coSimple(generatorFunc);

// 输出了想要的结果

// {name: “若川”}“generatorFunc-res”

模拟实现简版 co(第二版)

但是实际上,不会上面那么简单的。有可能是多个yield和传参数的情况。传参可以通过这如下两行代码来解决。

const args = Array.prototype.slice.call(arguments, 1);

gen = gen.apply(ctx, args);

两个yield,我大不了重新调用一下promise.then,搞定。

// 多个yeild,传参情况

function* generatorFunc(suffix = ‘’){

const res = yield request();

console.log(res, ‘generatorFunc-res’ + suffix);

const res2 = yield request();

console.log(res2, ‘generatorFunc-res-2’ + suffix);

}

function coSimple(gen){

const ctx = this;

const args = Array.prototype.slice.call(arguments, 1);

gen = gen.apply(ctx, args);

console.log(gen, ‘gen’);

const ret = gen.next();

const promise = ret.value;

promise.then(res => {

const ret = gen.next(res);

const promise = ret.value;

promise.then(res => {

gen.next(res);

});

});

}

coSimple(generatorFunc, ’ 哎呀,我真的是后缀’);

模拟实现简版 co(第三版)

问题是肯定不止两次,无限次的yield的呢,这时肯定要把重复的封装起来。而且返回是promise,这就实现了如下版本的代码。

function* generatorFunc(suffix = ‘’){

const res = yield request();

console.log(res, ‘generatorFunc-res’ + suffix);

const res2 = yield request();

console.log(res2, ‘generatorFunc-res-2’ + suffix);

const res3 = yield request();

console.log(res3, ‘generatorFunc-res-3’ + suffix);

const res4 = yield request();

console.log(res4, ‘generatorFunc-res-4’ + suffix);

}

function coSimple(gen){

const ctx = this;

const args = Array.prototype.slice.call(arguments, 1);

gen = gen.apply(ctx, args);

console.log(gen, ‘gen’);

return new Promise((resolve, reject) => {

onFulfilled();

function onFulfilled(res){

const ret = gen.next(res);

next(ret);

}

function next(ret) {

const promise = ret.value;

promise && promise.then(onFulfilled);

}

});

}

coSimple(generatorFunc, ’ 哎呀,我真的是后缀’);

但第三版的模拟实现简版co中,还没有考虑报错和一些参数合法的情况。

最终来看下co源码

这时来看看co的源码,报错和错误的情况,错误时调用reject,是不是就好理解了一些呢。

function co(gen) {

var ctx = this;

var args = slice.call(arguments, 1)

// we wrap everything in a promise to avoid promise chaining,

// which leads to memory leak errors.

// see https://github.com/tj/co/issues/180

return new Promise(function(resolve, reject) {

// 把参数传递给gen函数并执行

if (typeof gen === ‘function’) gen = gen.apply(ctx, args);

// 如果不是函数 直接返回

if (!gen || typeof gen.next !== ‘function’) return resolve(gen);

onFulfilled();

/**

  • @param {Mixed} res

  • @return {Promise}

  • @api private

*/

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。有需要面试题资料的朋友点击这里可以领取


f gen.next !== ‘function’) return resolve(gen);

onFulfilled();

/**

  • @param {Mixed} res

  • @return {Promise}

  • @api private

*/

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-lndrhQMw-1712710255106)]

[外链图片转存中…(img-nUfZJtti-1712710255107)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-FUAH4iso-1712710255107)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。有需要面试题资料的朋友点击这里可以领取

[外链图片转存中…(img-pn7gysUc-1712710255108)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值