Koa源码解析,带你实现一个迷你版的Koa
前言
本文是我在阅读 Koa
源码后,并实现迷你版 Koa
的过程。如果你使用过 Koa
但不知道内部的原理,我想这篇文章应该能够帮助到你,实现一个迷你版的 Koa
不会很难。
本文会循序渐进的解析内部原理,包括:
基础版本的 koa context 的实现 中间件原理及实现
文件结构
application.js
: 入口文件,里面包括我们常用的use
方法、listen
方法以及对ctx.body
做输出处理context.js
: 主要是做属性和方法的代理,让用户能够更简便的访问到request
和response
的属性和方法request.js
: 对原生的req
属性做处理,扩展更多可用的属性和方法,比如:query
属性、get
方法response.js
: 对原生的res
属性做处理,扩展更多可用的属性和方法,比如:status
属性、set
方法
基础版本
用法:
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
:
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.query
、ctx.set()
。但它们并不是 context
封装的,而是在访问 ctx
上的属性时,它内部通过属性劫持将 request
和 response
内封装的属性返回。就像你访问 ctx.query
,实际上访问的是 ctx.request.query
。
说到劫持你可能会想到 Object.defineProperty
,在 Kao
内部使用的是 ES6
提供的对象的 setter
和 getter
,效果也是一样的。所以要实现 ctx
,我们首先要实现 request
和 response
。
在此之前,需要修改下 createContext
方法:
// 这三个都是对象
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.query
、ctx.request.query
、ctx.app.query
等等的方式。
如果你觉得看起来有点冗余,也可以主要理解这几行,因为我们实现源码时也就用到下面这些:
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
:
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 来访问
*/
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
:
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
}
}
属性代理
通过上面的实现,我们可以使用 ctx.request.query
来访问到扩展的属性。但是在实际应用中,更常用的是 ctx.query
。不过 query
是在 request
的属性,通过 ctx.query
是无法访问的。
这时只需稍微做个代理,在访问 ctx.query
时,将 ctx.request.query
返回就可以实现上面的效果。
context.js
:
module.exports = {
get query() {
return this.request.query
}
}
实际的代码中会有很多扩展的属性,总不可能一个一个去写吧。为了优雅的代理属性,Koa
使用 delegates
包实现。这里我不打算用 delegates
,直接简单封装下代理函数。代理函数主要用到__defineGetter__
和 __defineSetter__
两个方法。
在对象上都会带有 __defineGetter__
和 __defineSetter__
,它们可以将一个函数绑定在当前对象的指定属性上,当属性被获取或赋值时,绑定的函数就会被调用。就像这样:
let obj = {}
let obj1 = {
name: 'JoJo'
}
obj.__defineGetter__('name', function(){
return obj1.name
})
此时访问 obj.name
,获取到的是 obj1.name
的值。
了解这个两个方法的用处后,接下来开始修改 context.js
:
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')
delegateMethod('response', 'set')
中间件原理
中间件思想是 Koa
最精髓的地方,为扩展功能提供很大的帮助。这也是它虽然小,却很强大的原因。还有一个优点,中间件使功能模块的职责更加分明,一个功能就是一个中间件,多个中间件组合起来成为一个完整的应用。
下面是著名的“洋葱模型”。这幅图很形象的表达了中间件思想的作用,它就像一个流水线一样,上游加工后的东西传递给下游,下游可以继续接着加工,最终输出加工结果。

原理分析
在调用 use
注册中间件的时候,内部会将每个中间件存储到数组中,执行中间件时,为其提供 next
参数。调用 next
即执行下一个中间件,以此类推。当数组中的中间件执行完毕后,再原路返回。就像这样:
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')
})
输出结果如下:
1 start
2 start
3 start
3 end
2 end
1 end
有点数据结构知识的同学,很快就想到这是一个“栈”结构,执行的顺序符合“先入后出”。
下面我将内部中间件实现原理进行简化,模拟中间件执行:
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()
执行过程:
调用 next1
,将其入栈执行,输出1 start
遇到 next2
函数,将其入栈执行,输出2 start
遇到 next3
函数,将其入栈执行,输出3 start
输出 3 end
,函数执行完毕,next3
弹出栈输出 2 end
,函数执行完毕,next2
弹出栈输出 1 end
,函数执行完毕,next1
弹出栈栈空,全部执行完毕
相信通过这个简单的例子,都大概明白中间件的执行过程了吧。
原理实现
中间件原理实现的关键点主要就是 ctx
和 next
的传递。
因为中间件是可以异步执行的,最后需要返回 Promise
。
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)
}
}
}
}
可以看到,实现过程本质是函数的递归调用。在内部实现时,其实 next
没有做什么神奇的操作,它就是下一个中间件调用的函数,作为参数传入供使用者调用。
下面我们来使用一下 compose
,你可以将它粘贴到控制台上运行:
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)
完整实现
application.js
:
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
本文使用 mdnice 排版
Koa源码解析,带你实现一个迷你版的Koa的更多相关文章
- EventBus (三) 源码解析 带你深入理解EventBus
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...
- Android EventBus源码解析 带你深入理解EventBus
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...
- Koa源码解析
Koa是一款设计优雅的轻量级Node.js框架,它主要提供了一套巧妙的中间件机制与简练的API封装,因此源码阅读起来也十分轻松,不论你从事前端或是后端研发,相信都会有所收获. 目录结构 首先将源码下载 ...
- Node.js躬行记(19)——KOA源码分析(上)
本次分析的KOA版本是2.13.1,它非常轻量,诸如路由.模板等功能默认都不提供,需要自己引入相关的中间件. 源码的目录结构比较简单,主要分为3部分,__tests__,lib和docs,从名称中就可 ...
- Spring IoC源码解析——Bean的创建和初始化
Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...
- Koa源码分析(一) -- generator
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: 1. Koa源码分析(一) -- generator 2. Koa源码分析(二) -- co的实现 ...
- koa源码阅读[3]-koa-send与它的衍生(static)
koa源码阅读的第四篇,涉及到向接口请求方提供文件数据. 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose第三篇:koa源码阅读-2-koa-router 处理静态 ...
- Koa源码分析(三) -- middleware机制的实现
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: Koa源码分析(一) -- generator Koa源码分析(二) -- co的实现 Koa源码分 ...
- koa源码阅读[2]-koa-router
koa源码阅读[2]-koa-router 第三篇,有关koa生态中比较重要的一个中间件:koa-router 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose k ...
随机推荐
- php的 mysqlnd驱动
这篇文章写的好 http://blog.linuxeye.com/395.html 传统编译php的时候需要指定mysql 的参数 --with-mysql=/usr/local/mysql \ -- ...
- 8.1Go并发
第八章 Go并发 Go语言区别于其他语言的一大特点就是出色的并发性能,最重要的一个特性那就是go关键字. 并发场景: UI小姐姐一边开着PS软件,一边微信疯狂的和产品经理打字交流,后台还听着网易云音乐 ...
- Hexo进阶设置
部署平台选型 前言 GitHub和Gitee(码云)是国内外比较流行的代码托管平台,现都推出GitHub/Gitee Pages可以存放静态网页代码,因此可以用来搭建自己的博客. 优缺点 平台 优点 ...
- 设计模式:Filter+Servlet+反射
传统设计 分类管理需要:增加,删除,编辑,修改,查询5个服务端功能. 一个路径对应一个Servlet的思路,就需要设计5个Servlet类,并且在web.xml中配置5个路径. CategoryAdd ...
- DOM变化后事件绑定失效
第一个file在change时,是能够触发事件的,而第二插入的file则没有change事件.对于这个问题,有如下两种解决方法: 第一种是将绑定change事件封装成一个函数,在点击button按钮插 ...
- GRpc添加客户端的四种方式
随着微服务的发展,相信越来越多的.net人员也开始接触GRpc这门技术,大家生成GRpc客户端的方式也各不相同,今天给大家介绍一下依据Proto文件生成Rpc客户端的四种方式 前提:需要安装4个Nug ...
- 实验五:shell脚本编程
项目 内容 这个作业属于哪个课程 班级课程的主页链接 这个作业的要求在哪里 作业要求链接地址 学号-姓名 17043133-木腾飞 作业学习目标 1.了解shell 脚本的概念及应用2.掌握shell ...
- 关于Java两点需要更新的知识
HashMap的初始容量 背景 很多人可以把HashMap的原理描述的很溜.比如JDK1.7之前,底层数据结构是数组+链表.JDK1.8之后,出于效率上的考虑,在数组长度大于64,链表长度大于8的时候 ...
- vue路由中使用keep-alive 以及activated和deactivated 钩子
本次只是记录下开发中碰到的问题. 最近做一个活动页面,涉及到角色和权限的问题,需要跳转很多页面,于是vue-router走起,顺便keep-alive也用起来了,嗯,跳转的很爽,但是一个详情页面组件, ...
- 从Point类继承的Circle类 代码参考
#include <iostream> #include <cstring> using namespace std; class Point { private: int x ...