【保姆级解析】我是如何从工作的视角看 Koa 源码的?,web开发敏捷之道

本文介绍了如何使用策略模式和Promise来解耦HTTP服务的接口和方法,遵循DRY原则,并逐步剖析了Koa框架中的中间件机制,包括洋葱模型和Koa-compose函数的实现,展示了如何构建和管理复杂的请求处理流程。
摘要由CSDN通过智能技术生成

)

但是一个服务不可能只有这么几个接口跟方法啊,总不能每加一个就增加一个分支吧,这样 handler 得变得多长多冗余,于是又很容易想到抽离 handler ,将 pathmethod 解耦。

1.2 策略模式解耦

如何解耦呢?从在新手村的代码中可以发现策略模式[2]刚好可以拿来解决这个问题:

const http = require(‘http’)

class Application {

constructor () {

// 收集route和method对应的回调函数

this.$handlers = new Map()

}

// 注册handler

register (method, path, handler) {

let pathInfo = null

if (this.$handlers.has(path)) {

pathInfo = this.$handlers.get(path)

} else {

pathInfo = new Map()

this.$handlers.set(path, pathInfo)

}

// 注册回调函数

pathInfo.set(method, handler)

}

use () {

return (request, response) => {

const { url: path, method } = request

this.KaTeX parse error: Expected 'EOF', got '&' at position 20: …lers.has(path) &̲& this.handlers.get(path).has(method)

? this.$handlers.get(path).get(method)(request, response)

: response.end(‘404 NOT FOUND!’)

}

}

}

const app = new Application()

app.register(‘GET’, ‘/’, (req, res) => {

res.end(‘Hello World!’)

})

app.register(‘GET’, ‘/about’, (req, res) => {

res.end(‘Hello About!’)

})

app.register(‘POST’, ‘/’, (req, res) => {

res.end(‘Post Method!’)

})

http

.createServer(app.use())

.listen(

8888,

() => {

console.log(‘listening 127.0.0.1:8888’)

}

)

1.3 符合DRY原则

但是这个时候就会发现:

  • 如果手抖把 method 方法写成了小写,因为 Http.Request.method 都是大写,无法匹配到正确的 handler ,于是返回 '404 NOT FOUND'

  • 如果我想在响应数据前增加一些操作,比如为每个请求增加一个时间戳,表示请求的时间,就必须修改每个 register 中的 handler 函数,不符合DRY原则

此时再修改一下上面的代码,利用 Promise 实现按顺序执行 handler

const http = require(‘http’)

class Application {

constructor() {

// 收集route和method对应的回调函数

this.$handlers = new Map()

// 暴露get和post方法

this.get = this.register.bind(this, ‘GET’)

this.post = this.register.bind(this, ‘POST’)

}

// 注册handler

register(method, path, …handlers) {

let pathInfo = null

if (this.$handlers.has(path)) {

pathInfo = this.$handlers.get(path)

} else {

pathInfo = new Map()

this.$handlers.set(path, pathInfo)

}

// 注册回调函数

pathInfo.set(method, handlers)

}

use() {

return (request, response) => {

const { url: path, method } = request

if (

this.$handlers.has(path) &&

this.$handlers.get(path).has(method)

) {

const _handlers = this.$handlers.get(path).get(method)

_handlers.reduce((pre, _handler) => {

return pre.then(() => {

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

_handler.call({}, request, response, () => {

resolve()

})

})

})

}, Promise.resolve())

} else {

response.end(‘404 NOT FOUND!’)

}

}

}

}

const app = new Application()

const addTimestamp = (req, res, next) => {

setTimeout(() => {

this.timestamp = Date.now()

next()

}, 3000)

}

app.get(‘/’, addTimestamp, (req, res) => {

res.end(‘Hello World!’ + this.timestamp)

})

app.get(‘/about’, addTimestamp, (req, res) => {

res.end(‘Hello About!’ + this.timestamp)

})

app.post(‘/’, addTimestamp, (req, res) => {

res.end(‘Post Method!’ + this.timestamp)

})

http

.createServer(app.use())

.listen(

8888,

() => {

console.log(‘listening 127.0.0.1:8888’)

}

)

1.4 降低用户心智

但是这样依旧有点小瑕疵,用户总是在重复创建 Promise,用户可能更希望无脑一点,那我们给用户暴露一个 next 方法,无论在哪里执行 next 就会进入下一个 handler,岂不美哉!!!

class Application {

// …

use() {

return (request, response) => {

const { url: path, method } = request

if (

this.$handlers.has(path) &&

this.$handlers.get(path).has(method)

) {

const _handlers = this.$handlers.get(path).get(method)

_handlers.reduce((pre, _handler) => {

return pre.then(() => {

return new Promise(resolve => {

// 向外暴露next方法,由用户决定什么时候进入下一个handler

_handler.call({}, request, response, () => {

resolve()

})

})

})

}, Promise.resolve())

} else {

response.end(‘404 NOT FOUND!’)

}

}

}

}

// …

const addTimestamp = (req, res, next) => {

setTimeout(() => {

this.timestamp = new Date()

next()

}, 3000)

}

2 Koa核心源码解析


上面的代码一路下来,基本上已经实现了一个简单中间件框架,用户可以在自定义中间件,然后在业务逻辑中通过 next() 进入下一个 handler,使得整合业务流程更加清晰。但是它只能推进中间件的执行,没有办法跳出中间件优先执行其他中间件。比如在koa中,一个中间件是类似这样的:

const Koa = require(‘koa’);

let app = new Koa();

const middleware1 = async (ctx, next) => {

console.log(1);

await next();

console.log(2);

}

const middleware2 = async (ctx, next) => {

console.log(3);

await next();

console.log(4);

}

const middleware3 = async (ctx, next) => {

console.log(5);

await next();

console.log(6);

}

app.use(middleware1);

app.use(middleware2);

app.use(middleware3);

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

ctx.body = ‘hello world’

})

app.listen(8888)

可以看到控制台输出的顺序是1, 3, 5, 6, 4, 2,这就是koa经典的洋葱模型。

接下来我们一步步解析koa的源码[3],可以看到总共只有4个文件,如果去掉注释,合起来代码也就1000多行。

| 文件 | 功能 |

| — | — |

| applicaiton.js | koa程序的入口,管理和调用中间件,处理http.createServer的回调,将请求的request和response代理至context上 |

| request.js | 对http.createServer回调函数中的request的封装,各种getter、setter以及额外属性 |

| response.js | 对http.createServer回调函数中的response的封装,各种getter、setter以及额外属性 |

| context.js | 代理request和response,并向外暴露一些功能 |

创建Koa实例的时候,Koa做的事情其实并不多,设置实例的一些配置,初始化中间件的队列,使用 Object.create 继承 contextrequestresponse

2.1 constructor

constructor(options) {

super();

// 实例的各种配置,不用太关注

options = options || {};

this.proxy = options.proxy || false;

this.subdomainOffset = options.subdomainOffset || 2;

this.proxyIpHeader = options.proxyIpHeader || ‘X-Forwarded-For’;

this.maxIpsCount = options.maxIpsCount || 0;

this.env = options.env || process.env.NODE_ENV || ‘development’;

if (options.keys) this.keys = options.keys;

// 最重要的实例属性,用于存放中间

this.middleware = [];

// 继承其他三个文件中的对象

this.context = Object.create(context);

this.request = Object.create(request);

this.response = Object.create(response);

}

因为Koa仅用于中间件的整合以及请求响应的监听,所以我们最关注的Koa的两个实例方法就是 uselisten。一个用来注册中间件,一个用来启动服务并监听端口。

2.2 use

功能非常简单,注册中间件,往实例属性middleware列表中推入中间件。

use(fn) {

if (typeof fn !== ‘function’) throw new TypeError(‘middleware must be a function!’);

// 利用co库转换generator函数,v3版本会移除,直接使用promise以及async…await

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);

// 用于链式注册中间件 app.use(xxx).use(xxx)…

return this;

}

2.3 listen

它的实现非常简单,就是直接调用 http.createServer 创建服务,并直接执行server.listen[4]的一些操作。稍微特殊一点地方是 createServer 传入的参数是调用实例方法 callback 的返回值。

listen(…args) {

debug(‘listen’);

// 创建服务

const server = http.createServer(this.callback());

// 透传参数,执行http模块的server.listen

return server.listen(…args);

}

2.4 callback

  • 调用 compose 方法,将所有中间件转换成 Promise 执行,并返回一个执行函数。

  • 调用父类 Emitter 中的 listenerCount 方法判断是否注册了 error 事件的监听器,若没有则为 error 事件注册 onerror 方法。

  • 定义传入 createServer 中的处理函数,这个处理函数有2个入参,分别是 requestresponse ,通过调用 createContext 方法把 requestresponse 封装成 ctx 对象,然后把 ctx 和第一步的执行函数 fn 传入 handleRequest 方法中。

callback() {

// 后面会讲解koa-compose,洋葱模型的核心,转换中间件的执行时机。

const fn = compose(this.middleware);

// 继承自Emitter,如果没有error事件的监听器,为error事件注册默认的事件监听方法onerror

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

//

const handleRequest = (req, res) => {

// 调用createContext方法把req和res封装成ctx对象

const ctx = this.createContext(req, res);

return this.handleRequest(ctx, fn);

};

return handleRequest;

}

2.5 createContext

createContext 的作用是将前面讲到的 contextrequestresponse 三个文件暴露出来的对象封装在一起,并额外增加app、req、res等,方便在ctx中获取各类信息。

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;

}

2.6 handleRequest

  • 获得res,将状态默认置为404

  • 定义失败的回调函数和中间件执行成功的回调函数,其中失败回调函数调用 context 中的 onerror 函数,不过最终还是触发app中注册的 onerror 函数;成功回调函数调用 respond 方法,读取 ctx 信息,把数据写入 res 中并响应请求。

  • 使用 on-finished 模块确保一个流在关闭、完成和报错时都会执行相应的回调函数。

  • 执行中间件函数 fnMiddleware,类似于 Promise.all,当全部中间件处理成功后,执行 handleResponse ,否则捕获异常。

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);

}

3 Koa-compose


koa-compose源码[5]非常简略:

  • 首先校验一下入参的合法性,最终返回一个函数。

  • 该函数内部使用 index 作为标识记录当前执行的中间,并返回从第一个中间件执行 dispatch 的结果。如果一个中间件内部多次执行 next() 方法,就会出现i的值等于 index,于是会报错 reject 掉。

  • 根据 index 取出中间件列表中的中间件,将 contextdispatch(i + 1) 中间件的入参 ctxnext 传入,当中间件执行 next() 方法时,就会按顺序执行下一个中间件,且将当前中间件放入执行栈中,最后当i等于中间件数组长度时候,即没有其他中间件了,就将入参 next(在Koa源码里是undefined)赋值给fn,此时fn未定义,于是返回空的 resolved 状态的 promise

  • 当最核心的中间件执行完成后,自然会触发 await 向下执行,开始执行上一个中间件,最终就形成了从外向里,再从里向外的洋葱模型。

// 入参是一个中间件列表,返回值是一个函数

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!’)

}

// 核心

return function (context, next) {

// 设置初始索引值

let index = -1

// 立即执行dispatch,传入0,并返回结果

return dispatch(0)

function dispatch (i) {

// 防止在一个中间件中多次调用next

if (i <= index) return Promise.reject(new Error(‘next() called multiple times’))
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

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

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

img

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

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

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

核心竞争力,怎么才能提高呢?

成年人想要改变生活,逆转状态?那就开始学习吧~

万事开头难,但是程序员这一条路坚持几年后发展空间还是非常大的,一切重在坚持。

为了帮助大家更好更高效的准备面试,特别整理了《前端工程师面试手册》电子稿文件。

前端面试题汇总

JavaScript

性能

linux

前端资料汇总

完整版PDF资料免费分享,只需你点赞支持,动动手指点击此处就可免费领取了

前端工程师岗位缺口一直很大,符合岗位要求的人越来越少,所以学习前端的小伙伴要注意了,一定要把技能学到扎实,做有含金量的项目,这样在找工作的时候无论遇到什么情况,问题都不会大。

s://img-blog.csdnimg.cn/img_convert/621960a57eb42479e02d6d64c0c81891.png)

前端面试题汇总

JavaScript

性能

linux

前端资料汇总

完整版PDF资料免费分享,只需你点赞支持,动动手指点击此处就可免费领取了

前端工程师岗位缺口一直很大,符合岗位要求的人越来越少,所以学习前端的小伙伴要注意了,一定要把技能学到扎实,做有含金量的项目,这样在找工作的时候无论遇到什么情况,问题都不会大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值