Koa源码解析,带你实现一个迷你版的Koa

前言#

本文是我在阅读 Koa 源码后,并实现迷你版 Koa 的过程。如果你使用过 Koa 但不知道内部的原理,我想这篇文章应该能够帮助到你,实现一个迷你版的 Koa 不会很难。

本文会循序渐进的解析内部原理,包括:

  1. 基础版本的 koa
  2. context 的实现
  3. 中间件原理及实现

文件结构#

  • application.js: 入口文件,里面包括我们常用的 use 方法、listen 方法以及对 ctx.body 做输出处理
  • context.js: 主要是做属性和方法的代理,让用户能够更简便的访问到requestresponse的属性和方法
  • request.js: 对原生的 req 属性做处理,扩展更多可用的属性和方法,比如:query 属性、get 方法
  • response.js: 对原生的 res 属性做处理,扩展更多可用的属性和方法,比如:status 属性、set 方法

基础版本#

用法:

Copy
const Coa = require('./coa/application') const app = new Coa() // 应用中间件 app.use((ctx) => { ctx.body = '<h1>Hello</h1>' }) app.listen(3000, '127.0.0.1')

application.js:

Copy
const http = require('http') module.exports = class Coa { use(fn) { this.fn = fn } // listen 只是语法糖 本身还是使用 http.createServer listen(...args) { const server = http.createServer(this.callback()) server.listen(...args) } callback() { const handleRequest = (req, res) => { // 创建上下文 const ctx = this.createContext(req, res) // 调用中间件 this.fn(ctx) // 输出内容 res.end(ctx.body) } return handleRequest } createContext(req, res) { let ctx = {} ctx.req = req ctx.res = res return ctx } }

基础版本的实现很简单,调用 use 将函数存储起来,在启动服务器时再执行这个函数,并输出 ctx.body 的内容。

但是这样是没有灵魂的。接下来,实现 context 和中间件原理,Koa 才算完整。

Context#

ctx 为我们扩展了很多好用的属性和方法,比如 ctx.queryctx.set()。但它们并不是 context 封装的,而是在访问 ctx 上的属性时,它内部通过属性劫持将 requestresponse 内封装的属性返回。就像你访问 ctx.query,实际上访问的是 ctx.request.query

说到劫持你可能会想到 Object.defineProperty,在 Kao 内部使用的是 ES6 提供的对象的 settergetter,效果也是一样的。所以要实现 ctx,我们首先要实现 requestresponse

在此之前,需要修改下 createContext 方法:

Copy
// 这三个都是对象 const context = require('./context') const request = require('./request') const response = require('./response') module.exports = class Coa { constructor() { this.context = context this.request = request this.response = response } createContext(req, res) { const ctx = Object.create(this.context) // 将扩展的 request、response 挂载到 ctx 上 // 使用 Object.create 创建以传入参数为原型的对象,避免添加属性时因为冲突影响到原对象 const request = ctx.request = Object.create(this.request) const response = ctx.response = Object.create(this.response) ctx.app = request.app = response.app = this; // 挂载原生属性 ctx.req = request.req = response.req = req ctx.res = request.res = response.res = res request.ctx = response.ctx = ctx; request.response = response; response.request = request; return ctx } }

上面一堆花里胡哨的赋值,是为了能通过多种途径获取属性。比如获取 query 属性,可以有 ctx.queryctx.request.queryctx.app.query 等等的方式。

如果你觉得看起来有点冗余,也可以主要理解这几行,因为我们实现源码时也就用到下面这些:

Copy
const request = ctx.request = Object.create(this.request) const response = ctx.response = Object.create(this.response) ctx.req = request.req = response.req = req ctx.res = request.res = response.res = res

request

request.js

Copy
const url = require('url') module.exports = { /* 查看这两步操作 * const request = ctx.request = Object.create(this.request) * ctx.req = request.req = response.req = req * * 此时的 this 是指向 ctx,所以这里的 this.req 访问的是原生属性 req * 同样,也可以通过 this.request.req 来访问 */ // 请求的 query 参数 get query() { return url.parse(this.req.url).query }, // 请求的路径 get path() { return url.parse(this.req.url).pathname }, // 请求的方法 get method() { return this.req.method.toLowerCase() } }

response

response.js

Copy
module.exports = { // 这里的 this.res 也和上面同理 // 返回的状态码 get status() { return this.res.statusCode }, set status(val) { return this.res.statusCode = val }, // 返回的输出内容 get body() { return this._body }, set body(val) { return this._body = val }, // 设置头部 set(filed, val) { if (typeof filed === 'string') { this.res.setHeader(filed, val) } if (toString.call(filed) === '[object Object]') { for (const key in filed) { this.set(key, filed[key]) } } } }

属性代理

通过上面的实现,我们可以使用 ctx.request.query 来访问到扩展的属性。但是在实际应用中,更常用的是 ctx.query。不过 query 是在 request 的属性,通过 ctx.query 是无法访问的。

这时只需稍微做个代理,在访问 ctx.query 时,将 ctx.request.query 返回就可以实现上面的效果。

context.js:

Copy
module.exports = { get query() { return this.request.query } }

实际的代码中会有很多扩展的属性,总不可能一个一个去写吧。为了优雅的代理属性,Koa 使用 delegates 包实现。这里我就直接简单封装下代理函数,代理函数主要用到__defineGetter____defineSetter__ 两个方法。

在对象上都会带有 __defineGetter____defineSetter__,它们可以将一个函数绑定在当前对象的指定属性上,当属性被获取或赋值时,绑定的函数就会被调用。就像这样:

Copy
let obj = {} let obj1 = { name: 'JoJo' } obj.__defineGetter__('name', function(){ return obj1.name })

此时访问 obj.name,获取到的是 obj1.name 的值。

了解这个两个方法的用处后,接下来开始修改 context.js

Copy
const proto = module.exports = { } // getter代理 function delegateGetter(prop, name){ proto.__defineGetter__(name, function(){ return this[prop][name] }) } // setter代理 function delegateSetter(prop, name){ proto.__defineSetter__(name, function(val){ return this[prop][name] = val }) } // 方法代理 function delegateMethod(prop, name){ proto[name] = function() { return this[prop][name].apply(this[prop], arguments) } } delegateGetter('request', 'query') delegateGetter('request', 'path') delegateGetter('request', 'method') delegateGetter('response', 'status') delegateSetter('response', 'status') delegateGetter('response', 'body') delegateSetter('response', 'body') delegateMethod('response', 'set')

中间件原理#

中间件思想是 Koa 最精髓的地方,为扩展功能提供很大的帮助。这也是它虽然小,却很强大的原因。还有一个优点,中间件使功能模块的职责更加分明,一个功能就是一个中间件,多个中间件组合起来成为一个完整的应用。

下面是著名的“洋葱模型”。这幅图很形象的表达了中间件思想的作用,它就像一个流水线一样,上游加工后的东西传递给下游,下游可以继续接着加工,最终输出加工结果。

原理分析

在调用 use 注册中间件的时候,内部会将每个中间件存储到数组中,执行中间件时,为其提供 next 参数。调用 next 即执行下一个中间件,以此类推。当数组中的中间件执行完毕后,再原路返回。就像这样:

Copy
app.use((ctx, next) => { console.log('1 start') next() console.log('1 end') }) app.use((ctx, next) => { console.log('2 start') next() console.log('2 end') }) app.use((ctx, next) => { console.log('3 start') next() console.log('3 end') })

输出结果如下:

Copy
1 start 2 start 3 start 3 end 2 end 1 end

有点数据结构知识的同学,很快就想到这是一个“栈”结构,执行的顺序符合“先入后出”。

下面我将内部中间件实现原理进行简化,模拟中间件执行:

Copy
function next1() { console.log('1 start') next2() console.log('1 end') } function next2() { console.log('2 start') next3() console.log('2 end') } function next3() { console.log('3 start') console.log('3 end') } next1()

执行过程:

  1. 调用 next1,将其入栈执行,输出 1 start
  2. 遇到 next2 函数,将其入栈执行,输出 2 start
  3. 遇到 next3 函数,将其入栈执行,输出 3 start
  4. 输出 3 end,函数执行完毕,next3 弹出栈
  5. 输出 2 end,函数执行完毕,next2 弹出栈
  6. 输出 1 end,函数执行完毕,next1 弹出栈
  7. 栈空,全部执行完毕

相信通过这个简单的例子,都大概明白中间件的执行过程了吧。

原理实现

中间件原理实现的关键点主要是 ctxnext 的传递。

Copy
function compose(middleware) { return function(ctx) { return dispatch(0) function dispatch(i){ // 取出中间件 let fn = middleware[i] if (!fn) { return } // dispatch.bind(null, i + 1) 为应用中间件接受到的 next // next 即下一个应用中间件 fn(ctx, dispatch.bind(null, i + 1)) } } }

可以看到,实现过程本质是函数的递归调用。在内部实现时,其实 next 没有做什么神奇的操作,它就是下一个中间件调用的函数,作为参数传入供使用者调用。

下面我们来单独测试 compose,你可以将它粘贴到控制台上运行:

Copy
function next1(ctx, next) { console.log('1 start') next() console.log('1 end') } function next2(ctx, next) { console.log('2 start') next() console.log('2 end') } function next3(ctx, next) { console.log('3 start') next() console.log('3 end') } let ctx = {} let fn = compose([next1, next2, next3]) fn(ctx)

最后,因为 Koa 中间件是可以使用 async/await 异步执行的,所以还需要修改下 compose 返回 Promise

Copy
function compose(middleware) { return function(ctx) { return dispatch(0) function dispatch(i){ // 取出中间件 let fn = middleware[i] if (!fn) { return Promise.resolve() } // dispatch.bind(null, i + 1) 为应用中间件接受到的 next // next 即下一个应用中间件 try { return Promise.resolve( fn(ctx, dispatch.bind(null, i + 1)) ) } catch (error) { return Promise.reject(error) } } } }

应用

实现完成中间件的逻辑后,将它应用到迷你版Koa中,原来的代码逻辑要做一些修改(部分代码忽略)

application.js:

Copy
module.exports = class Coa { constructor() { // 存储中间件的数组 this.middleware = [] } use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); // 将中间件加入数组 this.middleware.push(fn) return this } listen(...args) { const server = http.createServer(this.callback()) server.listen(...args) } callback() { const handleRequest = (req, res) => { // 创建上下文 const ctx = this.createContext(req, res) // fn 为第一个应用中间件 const fn = this.compose(this.middleware) // 在所有中间件执行完毕后 respond 函数用于处理 ctx.body 输出 return fn(ctx).then(() => respond(ctx)).catch(console.error) } return handleRequest } compose(middleware) { return function(ctx) { return dispatch(0) function dispatch(i){ let fn = middleware[i] if (!fn) { return Promise.resolve() } // dispatch.bind(null, i + 1) 为应用中间件接受到的 next try { return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1))) } catch (error) { return Promise.reject(error) } } } } } function respond(ctx) { let res = ctx.res let body = ctx.body if (typeof body === 'string') { return res.end(body) } if (typeof body === 'object') { return res.end(JSON.stringify(body)) } }

完整实现#

application.js:

Copy
const http = require('http') const context = require('./context') const request = require('./request') const response = require('./response') module.exports = class Coa { constructor() { this.middleware = [] this.context = context this.request = request this.response = response } use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); this.middleware.push(fn) return this } listen(...args) { const server = http.createServer(this.callback()) server.listen(...args) } callback() { const handleRequest = (req, res) => { // 创建上下文 const ctx = this.createContext(req, res) // fn 为第一个应用中间件 const fn = this.compose(this.middleware) return fn(ctx).then(() => respond(ctx)).catch(console.error) } return handleRequest } // 创建上下文 createContext(req, res) { const ctx = Object.create(this.context) // 处理过的属性 const request = ctx.request = Object.create(this.request) const response = ctx.response = Object.create(this.response) // 原生属性 ctx.app = request.app = response.app = this; ctx.req = request.req = response.req = req ctx.res = request.res = response.res = res request.ctx = response.ctx = ctx; request.response = response; response.request = request; return ctx } // 中间件处理逻辑实现 compose(middleware) { return function(ctx) { return dispatch(0) function dispatch(i){ let fn = middleware[i] if (!fn) { return Promise.resolve() } // dispatch.bind(null, i + 1) 为应用中间件接受到的 next // next 即下一个应用中间件 try { return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1))) } catch (error) { return Promise.reject(error) } } } } } // 处理 body 不同类型输出 function respond(ctx) { let res = ctx.res let body = ctx.body if (typeof body === 'string') { return res.end(body) } if (typeof body === 'object') { return res.end(JSON.stringify(body)) } }

写在最后#

本文的简单实现了 Koa 主要的功能。有兴趣最好还是自己去看源码,实现自己的迷你版 Koa。其实 Koa 的源码不算多,总共4个文件,全部代码包括注释也就 1800 行左右。而且逻辑不会很难,很推荐阅读,尤其适合源码入门级别的同学观看。

最后附上完整实现的代码:github

posted @   WahFung  阅读(737)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
Live2D
欢迎阅读『Koa源码解析,带你实现一个迷你版的Koa』
CONTENTS
点击右上角即可分享
微信分享提示