浅谈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 相关文章推荐
通过js脚本复制网页上的一个表格的不错实现方法
Dec 29 Javascript
JavaScript中清空数组的三种方法分享
Apr 07 Javascript
Javascript继承机制的设计思想分享
Aug 28 Javascript
找出字符串中出现次数最多的字母和出现次数精简版
Nov 07 Javascript
Javascript异步编程模型Promise模式详细介绍
May 08 Javascript
当滚动条滚动到页面底部自动加载增加内容的js代码
May 13 Javascript
原生javascript获取元素样式
Dec 31 Javascript
浅谈关于JavaScript API设计的一些建议和准则
Jun 24 Javascript
JS闭包与延迟求值用法示例
Dec 22 Javascript
微信小程序 input表单与redio及下拉列表的使用实例
Sep 20 Javascript
Vue EventBus自定义组件事件传递
Jun 25 Javascript
vue基于Echarts的拖拽数据可视化功能实现
Dec 04 Vue.js
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简单日历函数
2015/10/28 PHP
weiphp微信公众平台授权设置
2016/01/04 PHP
laravel-admin的图片删除实例
2019/09/30 PHP
javascript 模拟点击广告
2010/01/02 Javascript
精通JavaScript 纠正 cleanWhitespace函数
2010/03/11 Javascript
TimergliderJS 一个基于jQuery的时间轴插件
2011/12/07 Javascript
javascript控制swfObject应用介绍
2012/11/29 Javascript
js解析与序列化json数据(二)序列化探讨
2013/02/01 Javascript
通过url查找a元素应用案例
2014/04/29 Javascript
jquery结合CSS使用validate实现漂亮的验证
2015/01/29 Javascript
jQuery获取访问者IP地址的方法(基于新浪API与QQ查询接口)
2016/05/25 Javascript
js验证框架之RealyEasy验证详解
2016/06/08 Javascript
浅谈jQuery before和insertBefore的区别
2016/12/04 Javascript
基于Angularjs+mybatis实现二级评论系统(仿简书)
2017/02/13 Javascript
iOS + node.js使用Socket.IO框架进行实时通信示例
2017/04/14 Javascript
vue2过滤器模糊查询方法
2018/09/16 Javascript
浅谈在Vue.js中如何实现时间转换指令
2019/01/06 Javascript
js实现web调用摄像头 js截取视频画面
2019/04/21 Javascript
vuex中遇到的坑,vuex数据改变,组件中页面不渲染操作
2020/11/16 Javascript
Python使用pip安装报错:is not a supported wheel on this platform的解决方法
2018/01/23 Python
python3+requests接口自动化session操作方法
2018/10/13 Python
对python中GUI,Label和Button的实例详解
2019/06/27 Python
利用python实现短信和电话提醒功能的例子
2019/08/08 Python
python主线程与子线程的结束顺序实例解析
2019/12/17 Python
Pytorch 使用 nii数据做输入数据的操作
2020/05/26 Python
HTML5 常见面试题之PC端和移动端区别介绍
2018/01/22 HTML / CSS
纽约的奢华内衣店:Journelle
2016/07/29 全球购物
澳大利亚首屈一指的在线购物目的地:Kogan.com
2017/02/02 全球购物
实习销售业务员自我鉴定
2013/09/21 职场文书
高中生的自我鉴定范文
2014/01/24 职场文书
电气自动化个人求职信范文
2014/02/03 职场文书
十佳美德少年事迹材料
2014/02/05 职场文书
餐厅经理岗位职责范本
2014/02/17 职场文书
中学生综合素质自我评价
2015/03/06 职场文书
JUnit5常用注解的使用
2021/07/02 Java/Android
python字符串拼接.join()和拆分.split()详解
2021/11/23 Python