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

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

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

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

示例源码地址, 可以一边看源码,一边读文章,欢迎star!

本文适合对express ,koa ,redux 都有一定了解和使用经验的开发者阅读

服务端的中间件
express 和 koa 的中间件是用于处理 http 请求和响应的,但是二者的设计思路确不尽相同。大部分人了解的express和koa的中间件差异在于:

  • express采用“尾递归”方式,中间件一个接一个的顺序执行, 习惯于将response响应写在最后一个中间件中;
  • 而koa的中间件支持 generator, 执行顺序是“洋葱圈”模型。

所谓的“洋葱圈”模型:

不过实际上,express 的中间件也可以形成“洋葱圈”模型,在 next 调用后写的代码同样会执行到,不过express中一般不会这么做,因为 express的response一般在最后一个中间件,那么其它中间件 next() 后的代码已经影响不到最终响应结果了;

express

首先看一下 express的实现:

入口

// express.js

var proto = require('./application');
var mixin = require('merge-descriptors');

exports = module.exports = createApplication;

function createApplication() {

 // app 同时是一个方法,作为http.createServer的处理函数
 var app = function(req, res, next) {
  app.handle(req, res, next)
 }

 mixin(app, proto, false);
 return app
}

这里其实很简单,就是一个 createApplication 方法用于创建 express 实例,要注意返回值 app 既是实例对象,上面挂载了很多方法,同时它本身也是一个方法,作为 http.createServer的处理函数, 具体代码在 application.js中:

// application.js

var http = require('http');
var flatten = require('array-flatten');
var app = exports = module.exports = {}

app.listen = function listen() {
 var server = http.createServer(this)
 return server.listen.apply(server, arguments)
}

这里 app.listen 调用 nodejs 的http.createServer 创建web服务,可以看到这里 var server = http.createServer(this) 其中 this 即 app 本身, 然后真正的处理程序即 app.handle;

中间件处理

express 本质上就是一个中间件管理器,当进入到 app.handle 的时候就是对中间件进行执行的时候,所以,最关键的两个函数就是:

全局维护一个stack数组用来存储所有中间件,app.use 的实现就很简单了,可以就是一行代码 ``

// app.use
app.use = function(fn) {
 this.stack.push(fn)
}

express 的真正实现当然不会这么简单,它内置实现了路由功能,其中有 router, route, layer 三个关键的类,有了 router 就要对 path 进行分流,stack 中保存的是 layer实例,app.use 方法实际调用的是 router 实例的 use 方法, 有兴趣的可以自行去阅读。

app.handle 即对 stack 数组进行处理

app.handle = function(req, res, callback) {

 var stack = this.stack;
 var idx = 0;
 function next(err) {
 if (idx >= stack.length) {
 callback('err')
 return;
 }
 var mid;
 while(idx < stack.length) {
 mid = stack[idx++];
 mid(req, res, next);
 }
 }
 next()
}

这里就是所谓的"尾递归调用",next 方法不断的取出stack中的“中间件”函数进行调用,同时把next 本身传递给“中间件”作为第三个参数,每个中间件约定的固定形式为 (req, res, next) => {}, 这样每个“中间件“函数中只要调用 next 方法即可传递调用下一个中间件。

之所以说是”尾递归“是因为递归函数的最后一条语句是调用函数本身,所以每一个中间件的最后一条语句需要是next()才能形成”尾递归“,否则就是普通递归,”尾递归“相对于普通”递归“的好处在于节省内存空间,不会形成深度嵌套的函数调用栈。有兴趣的可以阅读下阮老师的尾调用优化

至此,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 打开注释,自己尝试一下。

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

(0)

相关推荐

  • 浅谈Redux中间件的实践

    最近项目前端开发框架采用React+Redux进行实现,但是,如何异步访问服务器端,以及想要在开发过程中进行状态树日志的输出,所以怎么才能解决这两个问题? 采用Redux中间件 为什么要使用中间件 在利用Redux进行状态管理时,用户在UI层面触发行为,一个action对象通过store.dispatch派发到Reducer进行触发,接下来Reducer会根据type来更新对应的Store上的状态树,更改后的state会触发对应组件的重新渲染.如下图所示.在这个流程中,action对象是一个同步

  • redux中间件之redux-thunk的具体使用

    redux的核心概念其实很简单:将需要修改的state都存入到store里,发起一个action用来描述发生了什么,用reducers描述action如何改变state tree .创建store的时候需要传入reducer,真正能改变store中数据的是store.dispatch API. 1.概念 dispatch一个action之后,到达reducer之前,进行一些额外的操作,就需要用到middleware.你可以利用 Redux middleware 来进行日志记录.创建崩溃报告.调用

  • 深入理解nodejs中Express的中间件

    Express是一个基于Node.js平台的web应用开发框架,在Node.js基础之上扩展了web应用开发所需要的基础功能,从而使得我们开发Web应用更加方便.更加快捷. 举一个例子: 用node.js实现一个控制台打印"hello server" var http = require('http'); var server = http.createServer(function(req,res){ console.log("hello server"); })

  • 浅谈express 中间件机制及实现原理

    简介 中间件机制可以让我们在一个给定的流程中添加一个处理步骤,从而对这个流程的输入或者输出产生影响,或者产生一些中作用.状态,或者拦截这个流程.中间件机制和tomcat的过滤器类似,这两者都属于责任链模式的具体实现. express 中间件使用案例 let express = require('express') let app = express() //解析request 的body app.use(bodyParser.json()) //解析 cookie app.use(cookieP

  • 深入理解 Koa 框架中间件原理

    Node 主要用在开发 Web 应用,koa 是目前 node 里最流行的 web 框架. 在 Node 开启一个 http 服务简直易如反掌,官网 demo. const http = require("http"); const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader("Content-Type", "text/plain&quo

  • nodejs开发——express路由与中间件

    路由 通常HTTP URL的格式是这样的: http://host[:port][path] http表示协议. host表示主机. port为端口,可选字段,不提供时默认为80. path指定请求资源的URI(Uniform Resource Identifier,统一资源定位符),如果URL中没有给出path,一般会默认成"/"(通常由浏览器或其它HTTP客户端完成补充上). 所谓路由,就是如何处理HTTP请求中的路径部分.比如"http://xxx.com/users/

  • node中koa中间件机制详解

    koa koa是由express原班人马打造的一个更小.更富有表现力.更健壮的web框架. 在我眼中,koa的确是比express轻量的多,koa给我的感觉更像是一个中间件框架,koa只是一个基础的架子,需要用到的相应的功能时,用相应的中间件来实现就好,诸如路由系统等.一个更好的点在于,express是基于回调来处理,至于回调到底有多么的不好,大家可以自行搜索来看.koa1基于的co库,所以koa1利用Generator来代替回调,而koa2由于node对async/await的支持,所以koa

  • express.js中间件说明详解

    express的新开发人员往往对路由处理程序和中间件之间的区别感到困惑.因此他们也对app.use(),app.all(),app.get(),app.post(),app.delete()和app.put()方法的区别感到困惑. 在本文中,我将解释中间件和路由处理程序之间的区别.以及如何正确使用app.use(),app.all(),app.get(),app.post(),app.delete()和app.put()方法. 路由处理 app.use(),app.all(),app.get()

  • 傻瓜式解读koa中间件处理模块koa-compose的使用

    最近需要单独使用到koa-compose这个模块,虽然使用koa的时候大致知道中间件的执行流程,但是没仔细研究过源码用起来还是不放心(主要是这个模块代码少,多的话也没兴趣去研究了). koa-compose看起来代码少,但是确实绕.闭包,递归,Promise...看了一遍脑子里绕不清楚.看了网上几篇解读文章,都是针对单行代码做解释,还是绕不清楚.最后只好采取一种傻瓜的方式: koa-compose去掉一些注释,类型校验后,源码如下: function compose (middleware) {

  • 详解express与koa中间件模式对比

    起因 最近在学习koa的使用, 由于koa是相当基础的web框架,所以一个完整的web应用所需要的东西大都以中间件的形式引入,比如koa-router, koa-view等.在koa的文档里有提到:koa的中间件模式与express的是不一样的,koa是洋葱型,express是直线型,至于为什么这样,网上很多文章并没有具体分析.或者简单的说是async/await的特性之类.先不说这种说法的对错,对于我来说这种说法还是太模糊了.所以我决定通过源码来分析二者中间件实现的原理以及用法的异同. 为了简

  • 简单介绍react redux的中间件的使用

    用过react的同学都知道在redux的存在,redux就是一种前端用来存储数据的仓库,并对改仓库进行增删改查操作的一种框架,它不仅仅适用于react,也使用于其他前端框架.研究过redux源码的人都觉得该源码很精妙,而本博文就针对redux中对中间件的处理进行介绍. 在讲redux中间件之前,先用两张图来大致介绍一下redux的基本原理: 图中就是redux的基本流程,这里就不细说. 一般在react中不仅仅利用redux,还利用到react-redux: react-redux这里也不细说.

  • Koa日志中间件封装开发详解

    对于一个服务器应用来说,日志的记录是必不可少的,我们需要使用其记录项目程序每天都做了什么,什么时候发生过错误,发生过什么错误等等,便于日后回顾.实时掌握服务器的运行状态,还原问题场景. 日志的作用 记录服务器程序运行状态: 帮助开发者快速捕获错误,定位以及决解故障. 日志中间件开发工具log4js 在node当中没有自带的日志模块,所以需要使用第三方模块 使用模块:log4js 安装: npm i log4js -S logsjs官方文档 日志分类: 访问日志: 记录客户端对项目的访问,主要是

随机推荐