分析洋葱模型实现原理,在自己项目中接入洋葱模型
上一篇文章初识洋葱模型,分析中间件执行过程,浅析koa中间件源码简单的介绍了 基于 koa 的洋葱模型的中间件的运行过程,了解了一下中间件的写法
不过基于 koa 的洋葱模型只有在发起请求的时候才能触发。那我们平时的项目中,如何使用洋葱模型?
简单分析 koa 代码
koajs/koa 代码分析:
先找 koa 入口文件
通过 package.json 文件中的 “main” 字段发现,入口文件在 lib/application.js
查看 application 中的执行流程
打开 application.js 查看 里面有几个熟悉的方法
koa 源码部分:
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
在实际项目中,我们调用 listen 通常是
app.listen('3000', function() {
console.log('创建监听成功')
})
代入 koa 源码中,可以看到一开始使用 http.createServer 创建了一个服务 然后我们传入的 3000 和 监听成功 的回调函数其实都是传给 server 的
http.createServer 这里接收的回调函数处理的则是触发请求的时候内容
node-api 文档 :http_createserver_options_requestlistener
额外小知识 在 http.createServer 传入的回调函数和 server.on(‘request’) 是一样的效果
所以当有请求进来的时候,执行的是 this.callback() 方法
callback () {
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
-
fn 是从 compose(this.middleware) 得到的,其中的 this.middleware 是一个函数数组,稍后会介绍到 -
listenerCount 方法应该是 Application 继承 Emitter 中的方法,暂时不深入研究 -
根据上面的代码调用的是 this.callback() ,返回的就是 handleRequest 函数了
handleRequest 中,创建了 ctx ,这个是专门为 koa 定制的一个响应的上下文
因为 http.createServer 的回调函数中就有 2 个返回值,分别是 req ,res 。所以 express 直接把这 2 个参数直接返回使用,而 koa 则是多包了一层
- 获取到 ctx 后,就进入了 handleRequest 部分
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)
}
- 这部分先把 statusCode 定义为 404(默认没有方法处理),后面有响应的话在转换为对应的状态码
handleResponse 是在上面传入的 fn 执行后就响应一次,到最后会触发 req.end() 方法,把内容响应出去onFinished 是从 on-finished 引入的一个库。用于监听一次响应结束,如果响应出错了,就执行对应的 onerror,引入这个应该就是为了监听请求时错误的内容了
从 this.callback 中可以知道 fnMiddleware = compose(this.middleware)
其中 compose 是 koa-compose
而 this.middleware 初始化的时候是个空数组,在 use 方法中 push 进去内容,熟悉 koa 的都知道, use 方法就是为了挂载中间件的
并且 use 函数特别简单,简单的判断了 fn 是否一个函数,然后就 push 进去,等待传递给 compose
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
application 粗略分析小结
- listen 方法创建监听
1.1 使用 http.createServer(this.callback()) 来创建请求的响应过程 - 在 callback 函数中,调用了
compose(this.middleware) 来获取一个可以执行的函数 fnMiddleware - 在
handleRequest 函数中,添加了错误监听,主要还是调用 fnMiddleware 函数,成功后就返回对应的值完成响应 this.middleware 来自于 use 函数,添加的中间件,所以 this.middleware 就是一个中间件列表,所以中间件的执行是按照顺序的
原来 koa 的中间件逻辑都是在 koajs/compose 这个库中,而 koa 只是调用了这个库
根据老规矩,看 package.json 文件,发现并没有 main 的入口字段,回到根目录发现其实整个库就只有一个 index.js 核心文件
洋葱模型全部代码~
不得不感叹大神写的代码总是那么的简洁却一环扣一环
'use strict'
module.exports = 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!')
}
return function(context, next) {
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)
}
}
}
}
简单说一下我分析的结果:
-
创建 compose,传入数组,然后 compose 会返回一个函数(也就是我们在 koa 源码中看到的 fnMiddleware),这时候这一批中间件执行顺序就已经确定好了 -
当执行 fn 的时候,就类似于一个 递归函数,不过他是通过回调函数的方式来实现的 代码非常简洁,就是定义了一个 dispatch 方法,然后 return dispatch(0) 当执行 dispatch 的时候,0 其实作为索引,按顺序取出数组中的对应的方法 fn = middleware[i] -
先说这里面的递归函数 fn(context, dispatch.bind(null, i + 1)) 翻译过来就是 fn_0(context,fn_1) 中间件当前函数执行的时候,把下一个函数也提上来了,调用 next 就是调用 fn_1,如果不调用 next 那当 fn_0 执行完成后,程序会继续往上走,这也就是为什么不调用 next 下一个中间件就不会执行 至于一开始 koa 支持 asymc/await 语法的巧妙之处就在于用 return Promise.resolve() 把普通的函数包装为 Promise 语法,那么调用 next 的时候就可以使用 .then 或者 await 了 -
其中最微妙的就是 if (i === middleware.length) fn = next 为什么 next 要赋值为 fn ? 因为当 i === middleware.length 的时候,中间件的数组已经都全部执行完了,next 函数取出来其实是 middleware[middleware.length] == null 这就会触发 if (!fn) return Promise.resolve() 可以理解为整个函数的"递归头"
总的来说,整个洋葱模型的实现就是把下一个方法提前到当前方法的 next 参数中,让你能决定下一个方法到底什么时候去调用,并且非常的灵活多变,比如下面的几种情况:
流水线模式
流水线就是一个方法执行完成给到下一个方法,比如下面的伪代码:
app.use(function(ctx, next) {
console.log('1')
next()
})
app.use(function(ctx, next) {
console.log('2')
next()
})
app.use(function(ctx, next) {
console.log('3')
next()
})
next 函数统一都放在最后调用,那么将会一次打印 1 2 3
经典的洋葱模式
app.use(function(ctx, next) {
console.log('1')
next()
console.log('1 - end')
})
app.use(function(ctx, next) {
console.log('2')
next()
console.log('2 - end')
})
app.use(function(ctx, next) {
console.log('3')
next()
console.log('3 - end')
})
在 next 函数前后都有函数处理,一层包裹一层,就像洋葱一样 打印结果为:
1
2
3
3 - end
2 - end
1 - end
倒序模式
app.use(function(ctx, next) {
next()
console.log('1')
})
app.use(function(ctx, next) {
next()
console.log('2')
})
app.use(function(ctx, next) {
next()
console.log('3')
})
这种就依次打印 3 2 1 了。
在自己项目中接入洋葱模型
通过前面一篇文章 初识洋葱模型,分析中间件执行过程,浅析koa中间件源码 加上上面的 koa 源码分析,洋葱模型想必也更加熟悉。如何接入自己的项目中去?
要想实现这目标,我们最起码得有一套函数来 收集和存储中间件方法(类似 koa 的 use),有一个触发中间件执行的方法(类似 koa 的 handleRequest),最后有一套中间件机制(直接使用 koa-compose)
- 那就是参数固定了只有 2 个:分别是
context 参数 和一个 next 回调参数 (当然你也可以魔改 koa-compose 完成自己想要的样子) context 需要为一个对象 Object 类型,因为引用数据类型有一个特点就是他们都指向同一内存地址,一个中间件中修改该数据,其他中间件也能即刻响应到,这样才能确保数据正常传输
比如下面的 Onion 就是实现了一个简易版的中间件触发器
通过 start 方法,把数据传入,然后进行一系列的处理,包括数据修改的日志打印,全大写字母转为首字母大写,句子第一个单词转换为大写
执行后效果如下:
const compose = require('koa-compose')
class Onion {
constructor() {
this.middleware = []
}
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
this.middleware.push(fn)
return this
}
start(context) {
const fnMiddleware = compose(this.middleware)
const handleRes = () => context
return fnMiddleware(context).then(handleRes)
}
}
let onion = new Onion()
function loggerMiddleware() {
return function(ctx, next) {
console.log(`转换前:${ctx.content}`)
next()
console.log(`转换后:${ctx.content}`)
}
}
function titleCase(text) {
return text.trim().replace(text[0], text[0].toUpperCase())
}
function titleCaseMiddleware() {
return function(ctx, next) {
ctx.content = titleCase(ctx.content)
next()
}
}
function lowercaseMiddleware() {
return function(ctx, next) {
ctx.content = ctx.content.replace(/[A-Z]+/g, function(str) {
return titleCase(str.toLowerCase())
})
next()
}
}
onion.use(loggerMiddleware())
onion.use(lowercaseMiddleware())
onion.use(titleCaseMiddleware())
var obj = { content: 'my name is JIOHO' }
onion.start(obj).then(res => {
console.log('================')
console.log('start 处理结果:', res)
})
关于中间件和洋葱模型的探究,这还只是入门级别,还有更多很微妙的处理就看实际的场景了~
|