2024年前端最全学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理,面试必备知识点总结

最后

面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

大厂面试题

面试题目录

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

*/

function onFulfilled(res) {

var ret;

try {

ret = gen.next(res);

} catch (e) {

return reject(e);

}

next(ret);

}

/**

  • @param {Error} err

  • @return {Promise}

  • @api private

*/

function onRejected(err) {

var ret;

try {

ret = gen.throw(err);

} catch (e) {

return reject(e);

}

next(ret);

最后

全网独播-价值千万金融项目前端架构实战

从两道网易面试题-分析JavaScript底层机制

RESTful架构在Nodejs下的最佳实践

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

一线互联网企业如何初始化项目-做一个自己的vue-cli

思维无价,看我用Nodejs实现MVC

代码优雅的秘诀-用观察者模式深度解耦模块

前端高级实战,如何封装属于自己的JS库

VUE组件库级组件封装-高复用弹窗组件

cHM6Ly9waWMyLnpoaW1nLmNvbS84MC92Mi1hY2UyZjVjNjQ1YjhkMTE1MzA4YzcyZDM1ZGNkZGYzNV9oZC5qcGc?x-oss-process=image/format,png)

一线互联网企业如何初始化项目-做一个自己的vue-cli

思维无价,看我用Nodejs实现MVC

代码优雅的秘诀-用观察者模式深度解耦模块

前端高级实战,如何封装属于自己的JS库

VUE组件库级组件封装-高复用弹窗组件

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值