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

栏目: Node.js · 发布时间: 5年前

内容简介:如果你有最开始接触本文主要对比

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

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

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

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

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

服务端的中间件

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

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

所谓的“洋葱圈”模型:

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

不过实际上, express 的中间件也可以形成“洋葱圈”模型,在 next 调用后写的代码同样会执行到,不过 express 中一般不会这么做,因为 expressresponse 一般在最后一个中间件,那么其它中间件 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 调用 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 打开注释,自己尝试一下。


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

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

跟我学Java Web

跟我学Java Web

2010-9 / 58.00元

Java Web开发是目前最流行、使用最广泛的网站开发技术。《跟我学Java Web》通过对Java Web开发中所运用到的各种技术循序渐进地进行讲解,使读者能尽快掌握开发Web应用程序的方法。《跟我学Java Web》内容包括搭建Web开发环境、HTML相关技术基础知识、JavaScript相关技术基础知识、JSP技术基础知识、Servlet技术基础知识、搭建MySQL数据库开发环境、JDBC技......一起来看看 《跟我学Java Web》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换