浅谈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实现动态CSS换肤技术的脚本
Jun 29 Javascript
jValidate 基于jQuery的表单验证插件
Dec 12 Javascript
jquery的ajaxSubmit()异步上传图片并保存表单数据演示代码
Jun 04 Javascript
禁止iframe页面的所有js脚本如alert及弹出窗口等
Sep 03 Javascript
JavaScript通过this变量快速找出用户选中radio按钮的方法
Mar 23 Javascript
Ajax中解析Json的两种方法对比分析
Jun 25 Javascript
基于Bootstrap使用jQuery实现简单可编辑表格
May 04 Javascript
详解webpack介绍&安装&常用命令
Jun 29 Javascript
vue中使用localstorage来存储页面信息
Nov 04 Javascript
解决angularjs WdatePicker ng-model的问题
Sep 13 Javascript
微信小程序实现单选功能
Oct 30 Javascript
JS常见错误(Error)及处理方案详解
Jul 02 Javascript
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处理斐波那契数列非递归方法
2012/02/04 PHP
wordpress自定义url参数实现路由功能的代码示例
2013/11/28 PHP
在CentOS上搭建LAMP+vsftpd环境的简单指南
2015/08/01 PHP
PHP小白必须要知道的php基础知识(超实用)
2017/10/10 PHP
Yii框架学习笔记之应用组件操作示例
2019/11/13 PHP
php 使用html5 XHR2实现上传文件与进度显示功能示例
2020/03/03 PHP
jquery中one()方法的用法实例
2015/01/16 Javascript
实例详解angularjs和ajax的结合使用
2015/10/22 Javascript
json格式数据的添加,删除及排序方法
2016/01/21 Javascript
AngularJs 弹出模态框(model)
2016/04/07 Javascript
下一代Bootstrap的5个特点 超酷炫!
2016/06/17 Javascript
Node.js 实现简单的接口服务器的实例代码
2017/05/23 Javascript
Vue添加请求拦截器及vue-resource 拦截器使用
2017/11/23 Javascript
微信小程序如何播放腾讯视频的实现
2019/09/20 Javascript
vue 项目打包时样式及背景图片路径找不到的解决方式
2019/11/12 Javascript
vue动态渲染svg、添加点击事件的实现
2020/03/13 Javascript
JavaScript 双向链表操作实例分析【创建、增加、查找、删除等】
2020/04/28 Javascript
Nest.js环境变量配置与序列化详解
2021/02/21 Javascript
python算法学习之桶排序算法实例(分块排序)
2013/12/18 Python
Python实现多线程下载文件的代码实例
2014/06/01 Python
python操作gmail实例
2015/01/14 Python
在Python的while循环中使用else以及循环嵌套的用法
2015/10/14 Python
python实现图片处理和特征提取详解
2017/11/13 Python
Python实现将通信达.day文件读取为DataFrame
2018/12/22 Python
tensorflow 保存模型和取出中间权重例子
2020/01/24 Python
python3操作注册表的方法(Url protocol)
2020/02/05 Python
Python 格式化打印json数据方法(展开状态)
2020/02/27 Python
解决tensorflow/keras时出现数组维度不匹配问题
2020/06/29 Python
美国主要的特色咖啡和茶公司:Peet’s Coffee
2020/02/14 全球购物
品牌宣传方案
2014/03/21 职场文书
2014年公务员工作总结
2014/11/18 职场文书
学习与创新自我评价
2015/03/09 职场文书
企业战略合作意向书
2015/05/08 职场文书
初中数学教学反思范文
2016/02/17 职场文书
MySQL一劳永逸永久支持输入中文的方法实例
2022/08/05 MySQL
Redis主从复制操作和配置详情
2022/09/23 Redis