浅谈redux, koa, express 中间件实现对比解析


Posted in Javascript onMay 23, 2019

如果你有 express ,koa, redux 的使用经验,就会发现他们都有 中间件(middlewares)的概念,中间件 是一种拦截器的思想,用于在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作。

最开始接触 中间件是在服务端使用 express 和 koa 的时候,后来从服务端延伸到前端,看到其在redux的设计中也得到的极大的发挥。中间件的设计思想也为许多框架带来了灵活而强大的扩展性。

本文主要对比redux, koa, express 的中间件实现,为了更直观,我会抽取出三者中间件相关的核心代码,精简化,写出模拟示例。示例会保持 express, koa,redux 的整体结构,尽量保持和源码一致,所以本文也会稍带讲解下express, koa, redux 的整体结构和关键实现:

至此,express 的中间件实现就完成了。

koa

不得不说,相比较 express 而言,koa 的整体设计和代码实现显得更高级,更精炼;代码基于ES6 实现,支持generator(async await), 没有内置的路由实现和任何内置中间件,context 的设计也很是巧妙。

整体

一共只有4个文件:

  • application.js 入口文件,koa应用实例的类
  • context.js ctx 实例,代理了很多request和response的属性和方法,作为全局对象传递
  • request.js koa 对原生 req 对象的封装
  • response.js koa 对原生 res 对象的封装

request.js 和 response.js 没什么可说的,任何 web 框架都会提供req和res 的封装来简化处理。所以主要看一下 context.js 和 application.js的实现

// context.js 

/**
 * Response delegation.
 */

delegate(proto, 'res')
 .method('setHeader')

/**
 * Request delegation.
 */

delegate(proto, 'req')
 .access('url')
 .setter('href')
 .getter('ip');

context 就是这类代码,主要功能就是在做代理,使用了 delegate 库。

简单说一下这里代理的含义,比如delegate(proto, 'res').method('setHeader') 这条语句的作用就是:当调用proto.setHeader时,会调用proto.res.setHeader 即,将proto的 setHeader方法代理到proto的res属性上,其它类似。

// application.js 中部分代码

constructor() {
 super()
 this.middleware = []
 this.context = Object.create(context)
}

use(fn) {
 this.middleware.push(fn)
}

listen(...args) {
 debug('listen')
 const server = http.createServer(this.callback());
 return server.listen(...args);
}

callback() {
 // 这里即中间件处理代码
 const fn = compose(this.middleware);
 
 const handleRequest = (req, res) => {
 // ctx 是koa的精髓之一, req, res上的很多方法代理到了ctx上, 基于 ctx 很多问题处理更加方便
 const ctx = this.createContext(req, res);
 return this.handleRequest(ctx, fn);
 };
 
 return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
 ctx.statusCode = 404;
 const onerror = err => ctx.onerror(err);
 const handleResponse = () => respond(ctx);
 return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

同样的在listen方法中创建 web 服务, 没有使用 express 那么绕的方式,const server = http.createServer(this.callback()); 用this.callback()生成 web 服务的处理程序

callback 函数返回handleRequest, 所以真正的处理程序是this.handleRequest(ctx, fn)

中间件处理

构造函数 constructor 中维护全局中间件数组 this.middleware和全局的this.context 实例(源码中还有request,response对象和一些其他辅助属性)。和 express 不同,因为没有router的实现,所有this.middleware 中就是普通的”中间件“函数而非复杂的 layer 实例,

this.handleRequest(ctx, fn); 中 ctx 为第一个参数,fn = compose(this.middleware) 作为第二个参数, handleRequest 会调用 fnMiddleware(ctx).then(handleResponse).catch(onerror); 所以中间处理的关键在compose方法, 它是一个独立的包koa-compose, 把它拿了出来看一下里面的内容:

// compose.js

'use strict'

module.exports = compose

function compose (middleware) {

 return function (context, next) {
 let index = -1
 return dispatch(0)
 function dispatch (i) {
  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)
  }
 }
 }
}

和express中的next 是不是很像,只不过他是promise形式的,因为要支持异步,所以理解起来就稍微麻烦点:每个中间件是一个async (ctx, next) => {}, 执行后返回的是一个promise, 第二个参数 next的值为 dispatch.bind(null, i + 1) , 用于传递”中间件“的执行,一个个中间件向里执行,直到最后一个中间件执行完,resolve 掉,它前一个”中间件“接着执行 await next() 后的代码,然后 resolve 掉,在不断向前直到第一个”中间件“ resolve掉,最终使得最外层的promise resolve掉。

这里和express很不同的一点就是koa的响应的处理并不在"中间件"中,而是在中间件执行完返回的promise resolve后:

return fnMiddleware(ctx).then(handleResponse).catch(onerror);

通过 handleResponse 最后对响应做处理,”中间件“会设置ctx.body, handleResponse也会主要处理 ctx.body ,所以 koa 的”洋葱圈“模型才会成立,await next()后的代码也会影响到最后的响应。

至此,koa的中间件实现就完成了。

redux

不得不说,redux 的设计思想和源码实现真的是漂亮,整体代码量不多,网上已经随处可见redux的源码解析,我就不细说了。不过还是要推荐一波官网对中间件部分的叙述 : redux-middleware

这是我读过的最好的说明文档,没有之一,它清晰的说明了 redux middleware 的演化过程,漂亮地演绎了一场从分析问题到解决问题,并不断优化的思维过程。

总体

本文还是主要看一下它的中间件实现, 先简单说一下 redux 的核心处理逻辑, createStore 是其入口程序,工厂方法,返回一个 store 实例,store实例的最关键的方法就是 dispatch , 而 dispatch 要做的就是一件事:

currentState = currentReducer(currentState, action)

即调用reducer, 传入当前state和action返回新的state。

所以要模拟基本的 redux 执行只要实现 createStore , dispatch 方法即可。其它的内容如 bindActionCreators, combineReducers 以及 subscribe 监听都是辅助使用的功能,可以暂时不关注。

中间件处理

然后就到了核心的”中间件" 实现部分即applyMiddleware.js:

// applyMiddleware.js

import compose from './compose'

export default function applyMiddleware(...middlewares) {
 return createStore => (...args) => {
 const store = createStore(...args)
 let dispatch = () => {
  throw new Error(
  `Dispatching while constructing your middleware is not allowed. ` +
   `Other middleware would not be applied to this dispatch.`
  )
 }

 const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
 }
 const chain = middlewares.map(middleware => middleware(middlewareAPI))
 dispatch = compose(...chain)(store.dispatch)

 return {
  ...store,
  dispatch
 }
 }
}

redux 中间件提供的扩展是在 action 发起之后,到达 reducer 之前,它的实现思路就和express 、 koa 有些不同了,它没有通过封装 store.dispatch, 在它前面添加 中间件处理程序,而是通过递归覆写 dispatch ,不断的传递上一个覆写的 dispatch 来实现。

每一个 redux 中间件的形式为 store => next => action => { xxx }

这里主要有两层函数嵌套:

最外层函数接收参数store, 对应于 applyMiddleware.js 中的处理代码是 const chain = middlewares.map(middleware => middleware(middlewareAPI)), middlewareAPI 即为传入的store 。这一层是为了把 store 的 api 传递给中间件使用,主要就是两个api:

  • getState, 直接传递store.getState.
  • dispatch: (...args) => dispatch(...args),这里的实现就很巧妙了,并不是store.dispatch, 而是一个外部的变量dispatch, 这个变量最终指向的是覆写后的dispatch, 这样做的原因在于,对于 redux-thunk 这样的异步中间件,内部调用store.dispatch 的时候仍然后走一遍所有“中间件”。

返回的chain就是第二层的数组,数组的每个元素都是这样一个函数next => action => { xxx }, 这个函数可以理解为 接受一个dispatch返回一个dispatch, 接受的dispatch 是后一个中间件返回的dispatch.

还有一个关键函数即 compose, 主要作用是 compose(f, g, h) 返回 () => f(g(h(..args)))

现在在来理解 dispatch = compose(...chain)(store.dispatch) 就相对容易了,原生的 store.dispatch 传入最后一个“中间件”,返回一个新的 dispatch , 再向外传递到前一个中间件,直至返回最终的 dispatch, 当覆写后的dispatch 调用时,每个“中间件“的执行又是从外向内的”洋葱圈“模型。

至此,redux中间件就完成了。

其它关键点

redux 中间件的实现中还有一点实现也值得学习,为了让”中间件“只能应用一次,applyMiddleware 并不是作用在 store 实例上,而是作用在 createStore 工厂方法上。怎么理解呢?如果applyMiddleware 是这样的

(store, middlewares) => {}

那么当多次调用 applyMiddleware(store, middlewares) 的时候会给同一个实例重复添加同样的中间件。所以 applyMiddleware 的形式是

(...middlewares) => (createStore) => createStore,

这样,每一次应用中间件时都是创建一个新的实例,避免了中间件重复应用问题。

这种形式会接收 middlewares 返回一个 createStore 的高阶方法,这个方法一般被称为 createStore的 enhance 方法,内部即增加了对中间件的应用,你会发现这个方法和中间件第二层 (dispatch) => dispatch 的形式一致,所以它也可以用于compose 进行多次增强。同时createStore 也有第三个参数enhance 用于内部判断,自增强。所以 redux 的中间件使用可以有两种写法:

第一种:用 applyMiddleware 返回 enhance 增强 createStore

store = applyMiddleware(middleware1, middleware2)(createStore)(reducer, initState)

第二种: createStore 接收一个 enhancer 参数用于自增强

store = createStore(reducer, initState, applyMiddleware(middleware1, middleware2))

第二种使用会显得直观点,可读性更好。

纵观 redux 的实现,函数式编程体现的淋漓尽致,中间件形式 store => next => action => { xx } 是函数柯里化作用的灵活体现,将多参数化为单参数,可以用于提前固定 store 参数,得到形式更加明确的 dispatch => dispatch,使得 compose得以发挥作用。

总结

总体而言,express 和 koa 的实现很类似,都是next 方法传递进行递归调用,只不过 koa 是promise 形式。redux 相较前两者有些许不同,先通过递归向外覆写,形成执行时递归向里调用。

总结一下三者关键异同点(不仅限于中间件):

  1. 实例创建: express 使用工厂方法, koa是类
  2. koa 实现的语法更高级,使用ES6,支持generator(async await)
  3. koa 没有内置router, 增加了 ctx 全局对象,整体代码更简洁,使用更方便。
  4. koa 中间件的递归为 promise形式,express 使用while 循环加 next 尾递归
  5. 我更喜欢 redux 的实现,柯里化中间件形式,更简洁灵活,函数式编程体现的更明显
  6. redux 以 dispatch 覆写的方式进行中间件增强

最后再次附上 模拟示例源码 以供学习参考,喜欢的欢迎star, fork!

回答一个问题

有人说,express 中也可以用 async function 作为中间件用于异步处理? 其实是不可以的,因为 express 的中间件执行是同步的 while 循环,当中间件中同时包含 普通函数 和 async 函数 时,执行顺序会打乱,先看这样一个例子:

function a() {
 console.log('a')
}

async function b() {
 console.log('b')
 await 1
 console.log('c')
 await 2
 console.log('d')
}

function f() {
 a()
 b()
 console.log('f')
}

这里的输出是 'a' > 'b' > 'f' > 'c'

在普通函数中直接调用async函数, async 函数会同步执行到第一个 await 后的代码,然后就立即返回一个promise, 等到内部所有 await 的异步完成,整个async函数执行完,promise 才会resolve掉.

所以,通过上述分析 express中间件实现, 如果用async函数做中间件,内部用await做异步处理,那么后面的中间件会先执行,等到 await 后再次调用 next 索引就会超出!,大家可以自己在这里 express async 打开注释,自己尝试一下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
使用javascript访问XML数据的实例
Dec 27 Javascript
javascript 隔行换色函数代码
Oct 24 Javascript
jQuery的观察者模式详解
Dec 22 Javascript
IE和Firefox之间在JavaScript语法上的差异
Apr 22 Javascript
功能强大的Bootstrap组件(结合js)
Aug 03 Javascript
Vue.js实现无限加载与分页功能开发
Nov 03 Javascript
JS遍历对象属性的方法示例
Jan 10 Javascript
使用Bootstrap + Vue.js实现添加删除数据示例
Feb 27 Javascript
vue.js中npm安装教程图解
Apr 10 Javascript
vue中倒计时组件的实例代码
Jul 06 Javascript
全面分析JavaScript 继承
May 30 Javascript
jQuery HTML设置内容和属性操作实例分析
May 20 jQuery
Express结合Webpack的全栈自动刷新
May 23 #Javascript
ajax跨域访问遇到的问题及解决方案
May 23 #Javascript
简单了解JavaScript异步
May 23 #Javascript
vue项目添加多页面配置的步骤详解
May 22 #Javascript
vue elementUI table 自定义表头和行合并的实例代码
May 22 #Javascript
微信小程序使用websocket通讯的demo,含前后端代码,亲测可用
May 22 #Javascript
微信小程序登录态和检验注册过没的app.js写法
May 22 #Javascript
You might like
php 代码优化之经典示例
2011/03/24 PHP
php入门学习知识点八 PHP中for循环基本应用之九九乘法口绝表
2011/07/14 PHP
linux使用crontab实现PHP执行计划定时任务
2014/05/10 PHP
让ThinkPHP支持大小写url地址访问的方法
2014/10/31 PHP
PHP实现算式验证码和汉字验证码实例
2015/03/09 PHP
表单验证的完整应用案例探讨
2013/03/29 Javascript
js复制到剪切板的实例方法
2013/06/28 Javascript
JS增加行复制行删除行的实现代码
2013/11/09 Javascript
禁用Tab键JS代码兼容Firefox和IE
2014/04/18 Javascript
ExtJS4 动态生成的grid导出为excel示例
2014/05/02 Javascript
javascript中attribute和property的区别详解
2014/06/05 Javascript
jquery常用方法及使用示例汇总
2014/11/08 Javascript
JQuery插件jcarousellite的参数中文说明
2015/05/11 Javascript
js实现div层缓慢收缩与展开的方法
2015/05/11 Javascript
基于jQuery插件实现环形图标菜单旋转切换特效
2015/05/15 Javascript
通过命令行生成vue项目框架的方法
2017/07/12 Javascript
vue使用drag与drop实现拖拽的示例代码
2017/09/07 Javascript
JS实现标签滚动切换效果
2017/12/25 Javascript
angular2 ng2-file-upload上传示例代码
2018/08/23 Javascript
vue-quill-editor富文本编辑器简单使用方法
2018/09/21 Javascript
详解Vue+Element的动态表单,动态表格(后端发送配置,前端动态生成)
2019/04/20 Javascript
解决VUE mounted 钩子函数执行时 img 未加载导致页面布局的问题
2020/07/27 Javascript
在vue项目中利用popstate处理页面返回的操作介绍
2020/08/06 Javascript
[01:00:54]TI4正赛第二日开场
2014/07/20 DOTA
初步讲解Python中的元组概念
2015/05/21 Python
利用python批量检查网站的可用性
2016/09/09 Python
Python向excel中写入数据的方法
2019/05/05 Python
django 环境变量配置过程详解
2019/08/06 Python
Python从文件中读取指定的行以及在文件指定位置写入
2019/09/06 Python
pandas 按日期范围筛选数据的实现
2021/02/20 Python
解析HTML5的存储功能和web SQL的相关操作方法
2016/02/19 HTML / CSS
初三化学教学反思
2014/01/23 职场文书
学校安全责任书范本
2014/07/23 职场文书
2019年度开业庆典祝福语大全!
2019/07/05 职场文书
关于Nginx中虚拟主机的一些冷门知识小结
2022/03/03 Servers
Python Matplotlib绘制动画的代码详解
2022/05/30 Python