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

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

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

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

示例源码地址, 能够一边看源码,一边读文章,欢迎star!git

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

服务端的中间件

expresskoa 的中间件是用于处理 http 请求和响应的,可是两者的设计思路确不尽相同。大部分人了解的expresskoa的中间件差别在于:web

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

所谓的“洋葱圈”模型:express

不过实际上,express 的中间件也能够造成“洋葱圈”模型,在 next 调用后写的代码一样会执行到,不过express中通常不会这么作,由于 expressresponse通常在最后一个中间件,那么其它中间件 next() 后的代码已经影响不到最终响应结果了;编程

express

首先看一下 express 的实现:redux

入口

// 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 中:api

// 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 调用 nodejshttp.createServer 建立web服务,能够看到这里 var server = http.createServer(this) 其中 thisapp 自己, 而后真正的处理程序即 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 实例,代理了不少requestresponse的属性和方法,做为全局对象传递
  • request.js koa 对原生 req 对象的封装
  • response.js koa 对原生 res 对象的封装

request.jsresponse.js 没什么可说的,任何 web 框架都会提供reqres 的封装来简化处理。因此主要看一下 context.jsapplication.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 即,将protosetHeader方法代理到protores属性上,其它相似。

// 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, 传入当前stateaction返回新的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 以前,它的实现思路就和expresskoa 有些不一样了,它没有经过封装 store.dispatch, 在它前面添加 中间件处理程序,而是经过递归覆写 dispatch ,不断的传递上一个覆写的 dispatch 来实现。

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

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

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

    1. getState, 直接传递store.getState.
    2. 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 的高阶方法,这个方法通常被称为 createStoreenhance 方法,内部即增长了对中间件的应用,你会发现这个方法和中间件第二层 (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得以发挥做用。

总结

整体而言,expresskoa 的实现很相似,都是next 方法传递进行递归调用,只不过 koapromise 形式。redux 相较前二者有些许不一样,先经过递归向外覆写,造成执行时递归向里调用。

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

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

最后再次附上 模拟示例源码 以供学习参考,喜欢的欢迎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 打开注释,本身尝试一下。

相关文章
相关标签/搜索