前言

本文是我在阅读 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 方法

基础版本

用法:

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.queryctx.set()。但它们并不是 context 封装的,而是在访问 ctx 上的属性时,它内部通过属性劫持将 requestresponse 内封装的属性返回。就像你访问 ctx.query,实际上访问的是 ctx.request.query

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

在此之前,需要修改下 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.queryctx.request.queryctx.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()

执行过程:

  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 的传递。

因为中间件是可以异步执行的,最后需要返回 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的更多相关文章

  1. EventBus (三) 源码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  2. Android EventBus源码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  3. Koa源码解析

    Koa是一款设计优雅的轻量级Node.js框架,它主要提供了一套巧妙的中间件机制与简练的API封装,因此源码阅读起来也十分轻松,不论你从事前端或是后端研发,相信都会有所收获. 目录结构 首先将源码下载 ...

  4. Node.js躬行记(19)——KOA源码分析(上)

    本次分析的KOA版本是2.13.1,它非常轻量,诸如路由.模板等功能默认都不提供,需要自己引入相关的中间件. 源码的目录结构比较简单,主要分为3部分,__tests__,lib和docs,从名称中就可 ...

  5. Spring IoC源码解析——Bean的创建和初始化

    Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...

  6. Koa源码分析(一) -- generator

    Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: 1. Koa源码分析(一) -- generator 2. Koa源码分析(二) -- co的实现 ...

  7. koa源码阅读[3]-koa-send与它的衍生(static)

    koa源码阅读的第四篇,涉及到向接口请求方提供文件数据. 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose第三篇:koa源码阅读-2-koa-router 处理静态 ...

  8. Koa源码分析(三) -- middleware机制的实现

    Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: Koa源码分析(一) -- generator Koa源码分析(二) -- co的实现 Koa源码分 ...

  9. koa源码阅读[2]-koa-router

    koa源码阅读[2]-koa-router 第三篇,有关koa生态中比较重要的一个中间件:koa-router 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose k ...

随机推荐

  1. webpack指南(六)命令行环境配置

    webpack 命令行环境配置中,通过设置 --env 可以使你根据需要,传入尽可能多的环境变量.在 webpack.config.js 文件中可以访问到这些环境变量. webpack --env.N ...

  2. Angular路由知识点

    路由跳转 1. 模板方式:<ANY  routerLink='/ucenter'></ANY> 2. 脚本方式:  constructor(private router:Rou ...

  3. django提供二进制流数据文件的下载

    基于djnago框架的二进制流数据传输(提供较大文件的下载) (1)数据源: 高质量图片.视频.音频.文件.数据库数据等.如果是数据库文件,需要先读取相应的数据,然后写入表格在传输到前端以供下载! ( ...

  4. INNODB索引单列不能超767 复合不能超3072

    innodb复合索引长度为什么是3072  我们知道InnoDB一个page的默认大小是16k.由于是Btree组织,要求叶子节点上一个page至少要包含两条记录(否则就退化链表了).         ...

  5. sql语句 怎么从一张表中查询数据插入到另一张表中?

    sql语句 怎么从一张表中查询数据插入到另一张表中?  ----原文地址:http://www.phpfans.net/ask/MTc0MTQ4Mw.html 比如我有两张表 table1 字段 un ...

  6. 使用PHP得到所有的HTTP请求头

    作者:老王 在PHP里,想要得到所有的HTTP请求头,可以使用getallheaders方法,不过此方法并不是在任何环境下都存在,比如说,你使用fastcgi方式运行PHP的话,就没有这个方法,所以说 ...

  7. python遍历

    实现遍历: #coding=utf-8 #遍历的2种方式 import os #1.使用os.listdir(f) def traverse(f): fs = os.listdir(f) for f1 ...

  8. 【JavaScript数据结构系列】07-循环链表CircleLinkedList

    [JavaScript数据结构系列]07-循环链表CircleLinkedList 码路工人 CoderMonkey 转载请注明作者与出处 1. 认识循环链表 首节点与尾节点相连的,就构成循环链表.其 ...

  9. redis使用技巧十连胜,学会工作六到飞起

    Redis 在当前的技术社区里是非常热门的.从来自 Antirez 一个小小的个人项目到成为内存数据存储行业的标准,Redis已经走过了很长的一段路. 随之而来的一系列最佳实践,使得大多数人可以正确地 ...

  10. P2812 校园网络

    luogu 传送门 首先考虑问题一 不难想到,如果有一个学校作为终端机,那么跟其处于同一个强联通中的所有学校就可以不用作为终端机了. 那么,问题一也就迎刃而解了:找到所有入度为0的缩点.因为这个学校( ...