分析洋葱模型实现原理,在自己项目中接入洋葱模型

8 篇文章 0 订阅

分析洋葱模型实现原理,在自己项目中接入洋葱模型

上一篇文章初识洋葱模型,分析中间件执行过程,浅析koa中间件源码简单的介绍了 基于 koa 的洋葱模型的中间件的运行过程,了解了一下中间件的写法

不过基于 koa 的洋葱模型只有在发起请求的时候才能触发。那我们平时的项目中,如何使用洋葱模型?

简单分析 koa 代码

koajs/koa 代码分析:

先找 koa 入口文件

通过 package.json 文件中的 “main” 字段发现,入口文件在 lib/application.js

查看 application 中的执行流程

打开 application.js 查看
里面有几个熟悉的方法

  • listen 方法

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 方法
// compose 来自引入的 koa-compose
// const compose = require('koa-compose')

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
}
  1. fn 是从 compose(this.middleware) 得到的,其中的 this.middleware 是一个函数数组,稍后会介绍到

  2. listenerCount 方法应该是 Application 继承 Emitter 中的方法,暂时不深入研究

  3. 根据上面的代码调用的是 this.callback(),返回的就是 handleRequest 函数了

handleRequest 中,创建了 ctx ,这个是专门为 koa 定制的一个响应的上下文

因为 http.createServer 的回调函数中就有 2 个返回值,分别是 reqres。所以 express 直接把这 2 个参数直接返回使用,而 koa 则是多包了一层

  1. 获取到 ctx 后,就进入了 handleRequest 部分
  • 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)
}
  1. 这部分先把 statusCode 定义为 404(默认没有方法处理),后面有响应的话在转换为对应的状态码
  2. handleResponse 是在上面传入的 fn 执行后就响应一次,到最后会触发 req.end() 方法,把内容响应出去
  3. onFinished 是从 on-finished 引入的一个库。用于监听一次响应结束,如果响应出错了,就执行对应的 onerror,引入这个应该就是为了监听请求时错误的内容了
  • 分析 fnMiddleware 来源

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 粗略分析小结

  1. listen 方法创建监听
    1.1 使用 http.createServer(this.callback()) 来创建请求的响应过程
  2. 在 callback 函数中,调用了 compose(this.middleware) 来获取一个可以执行的函数 fnMiddleware
  3. handleRequest 函数中,添加了错误监听,主要还是调用 fnMiddleware 函数,成功后就返回对应的值完成响应
  4. this.middleware 来自于 use 函数,添加的中间件,所以 this.middleware 就是一个中间件列表,所以中间件的执行是按照顺序的

koa-compose 分析

原来 koa 的中间件逻辑都是在 koajs/compose 这个库中,而 koa 只是调用了这个库

根据老规矩,看 package.json 文件,发现并没有 main 的入口字段,回到根目录发现其实整个库就只有一个 index.js 核心文件

洋葱模型全部代码~

不得不感叹大神写的代码总是那么的简洁却一环扣一环

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function(context, next) {
    // last called middleware #
    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)
      }
    }
  }
}

简单说一下我分析的结果:

  1. 创建 compose,传入数组,然后 compose 会返回一个函数(也就是我们在 koa 源码中看到的 fnMiddleware),这时候这一批中间件执行顺序就已经确定好了

  2. 当执行 fn 的时候,就类似于一个 递归函数,不过他是通过回调函数的方式来实现的
    代码非常简洁,就是定义了一个 dispatch 方法,然后 return dispatch(0)
    当执行 dispatch 的时候,0其实作为索引,按顺序取出数组中的对应的方法 fn = middleware[i]

  3. 先说这里面的递归函数
    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 了

  4. 其中最微妙的就是 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)

  • 使用 koa-compose 有什么限制?
  1. 那就是参数固定了只有 2 个:分别是 context 参数 和一个 next 回调参数 (当然你也可以魔改 koa-compose 完成自己想要的样子)
  2. 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)
})

关于中间件和洋葱模型的探究,这还只是入门级别,还有更多很微妙的处理就看实际的场景了~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值