前端常见中间件机制对比分析

2,120 阅读5分钟

前言

在日常的的开发中,我们有用到很多的库,这些库不仅功能强大,而且配置灵活,对外提供接口来让我们自己来实现部分逻辑分层,来满足业务中千奇百怪的业务需求,这就是大家常说的中间件机制。 本文将以koaexpressaxiosredux为例,来了解前端常见的几种中间件实现方式。

Koa

Koa的洋葱模型想必大家都耳熟能详了,这种灵活的中间机制koa能够处理复杂的业务需求,并且还有自带逻辑解耦特性,可以说是koa备受推崇的原因了。

koa

从上面的图,我们可以了解到洋葱的每一层就对应着一个中间件,外层的中间件嵌套着内层的中间件,请求从外层中间件进入,层层处理后的返回值,又一层层传递出去。所以外层的中间件可以控制内层中间件的执行,并且影响内层中间件的request和response,而内层的中间件只能处理外层的响应阶段。 洋葱模型本质上是高阶函数的嵌套。 我们来看看源码中的koa类:

class Application extends Emitter {
  constructor(options) {
    super()
    this.middleware = [];
  }

  createContext(req, res) {
    const context = Object.create(this.context || {});
  	// ...
    context.req = req;
    context.res = res;
    context.state = {};
    return context;
  }
// 处理请求入口
  callback() {
    const fn = compose(this.middleware);
    // if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      // 处理请求
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    this.middleware.push(fn);
    return this;
  }

  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  onerror(err) {
  }

}

可以看到use方法就是把中间件函数推入一个数组中,接下来是调用koa-compose导出的compose函数把中间件组装起来。

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // index向后移动
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      // 最后一个中间件调用next 也不会报错
      if (!fn) return Promise.resolve()
      try {
        // 传递ctx, dispatch ——》 next方法,进入下一个中间件。
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

简单来说,dispatch(n)对应着第 n 个中间件的执行,而 dispatch(n)又可以控制是否执行 dispatch(n + 1)。那么它是怎么处理错误的呢?

当n为0时,dispatch(0)包含着 dispatch(1),而 dispatch(1)又包含着 dispatch(2) 在这个模式下,我们使用try catch,它可以 catch 住函数以及函数内部继续调用的函数的所有error,所以我们只需把第一个中间件作为错误中间件就可以了。

express

express相对于koa来说,集成了路由配置,静态服务器和模板引擎等功能,它也是最早的基于node的框架,那么它的是怎么实现中间件功能和路由的匹配呢?

路由匹配 这里要区分一下路由中间件和普通中间件的概念。先看下路由匹配的整体流程图: express route

express 入口文件

const mixin = require('merge-descriptors');
const EventEmitter = require('events').EventEmitter;
const proto = require('./application');
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);
  
  app.init();
  return app;
}

module.exports = createApplication;

application.js

const http = require('http');
const flatten = require('array-flatten');
const methods = require('methods');
const finalhandler = require('finalhandler');
const Router = require('./router');

const app = exports = module.exports = {};

const slice = Array.prototype.slice;

app.init = function() {}
// 在app 上挂在router对象
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({});
  }
};

app.use = function(fn) {
  var offset = 0;
  var path = '/';
  // 得到中间件的路径,和中间件处理函数 数组
  if (typeof fn !== 'function') {
    var arg = fn;
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }
  // 扁平参数 
  var fns = flatten(slice.call(arguments, offset));
  if (fns.length === 0) {
    throw new TypeError('app.use() requires a middleware function')
  }
  // 挂载 router对象
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function(fn) {
    // 中间件函数
    if (!fn || !fn.handle || !fn.set) {
      // 中间件存入 router对象中的stack
      return router.use(path, fn);
    }

  });
}

app.handle = function(req, res, callback) {
  var router = this._router;
  // final handler
  var done = callback || finalhandler(req, res, {
  });
  // 交由router对象处理
  router.handle(req, res, done);
}

app.route = function route(path) {
  this.lazyrouter();
  return this._router.route(path);
};

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

router.js

var proto = module.exports = function (options) {
  var opts = options || {};
  function router(req, res, next) {
    router.handle(req, res, next);
  }
  Object.setPrototypeOf(router, proto);
  // 存储中间件
  router.stack = [];
  return router;
}
// 和 app.use的处理逻辑类似
proto.use = function(fn) {
  var offset = 0;
  var path = '/';
// 扁平参数
  var callbacks = flatten(slice.call(arguments, offset));
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
	// layer 记录路径 参数 和中间件函数
    // 最后中间件函数也是 由 layer提供的handle_request 方法执行
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);
    // 普通中间件,匹配路径就已可以执行
    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
}

可以概括为,当客户端发送一个http请求后,会先进入express实例对象对应的router.handle函数中,router.handle函数会通过next()遍历stack中的每一个layer进行match,如果match返回true,则获取layer.route。如果没有layer.route则是普通的中间件,我们直接直接执行layer.handle_request; 否则是路由中间件,那么需要执行route.dispatch函数,route.dispatch同样是通过next()遍历stack中的每一个layer,然后执行layer.handle_request,也就是调用中间件函数。直到所有的中间件函数被执行完毕,整个路由处理结束。

路由中间件主要存在Route 对象的stack属性中。 Route.js

function Route(path) {
  this.path = path;
  // 匹配路径,路由中间件处理函数
  this.stack = [];
  this.methods = {};
}
// 依次处理stack中的函数
Route.prototype.dispatch = function(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  if (stack.length === 0) {
    // 这里的done 是 router 对象handle中的next方法
    // stack中的函数执行完成后, 调用 done 继续执行存在router.stack中的函数
    return done();
  }
// ...

  next();
  function next(err) {
    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }
    if (layer.method && layer.method !== method) {
      return next(err);
    }
    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
}

layer.js

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }
// ...
  this.handle = fn;
}
// 处理错误
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;
  if (fn.length !== 4) {
    // not a standard error handler
    // 继续传递错误
    return next(error);
  }
  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};
// 处理中间件
Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;
  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }
  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};


中间件的执行流程如下:

axios

axios中的拦截器相信大家都不会陌生吧。它支持请求拦截器和响应拦截器,每个拦截器接受到成功态处理函数和失败态处理函数,这里有么有联想到promise.then呢?

axios的拦截器执行流程可以概括如下:

那么我们来看看axios拦截器是怎么实现的呢? InterceptorManager.js

function InterceptorManager() {
  this.handlers = [];
}
// 添加拦截器
InterceptorManager.prototype.use = function(fulfilled, rejected) {
  this.handlers.push({
    fulfilled,
    rejected
  })

  return this.handlers.length - 1;
}
// 请除拦截器
InterceptorManager.prototype.eject = function(id) {
  if(this.handlers[id]) {
    this.handlers[id] = null;
  }
}

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if(h !== null) {
      fn(h);
    }
  })
}

axios核心对象

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
  	// 请求拦截器对象
    request: new InterceptorManager(),
    // 响应拦截器对象
    response: new InterceptorManager()
  };
}
// 请求方法
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // config = mergeConfig(this.defaults, config);
  
  // var chain = [dispatchRequest, undefined];
  // 模拟请求异步
  const chain = [() => new Promise((resolve,reject) =>{
    setTimeout(()=>{
      resolve('发送请求, 获取数据');
    } , 500)
  }).then(res=>{
    console.log(res)
  }) , undefined];
  let promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 添加请求拦截, 成功态, 失败态
    chain.unshift(interceptor.fulfilled, interceptor.rejected)
  })

  this.interceptors.response.forEach(function (interceptor) {
    // 添加响应拦截
    chain.push(interceptor.fulfilled, interceptor.rejected);
  })

  // 执行拦截器 promise链,发送请求,
  while(chain.length) {
    promise = promise.then(chain.shift(), chain.shift())
  }
}

redux

想要了解redux的中间件机制,我们需要先看懂一个方法compose:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

简单理解的话,就是compose(fn1, fn2, fn3) (...args) = > fn1(fn2(fn3(...args))) 它是一种高阶聚合函数,相当于把 fn3 先执行,然后把结果传给 fn2 再执行,再把结果交给 fn1 去执行。

那么redux的执行流程是怎样的呢?

可以看到,state和dispatch构成了我们的仓库,我们可以向仓库发送action通过reducer改变状态,状态改变之后可以修改视图,用户可以通过鼠标点击视图,视图派发action,改变状态,形成循环,有时候我们需要发布异步操作,想在派发前,派发后做一些额外动作,此时我们就需要插入中间件,我们的方法就是得到dispatch方法,重写dispacth。redux 中间件的机制可以用一句话来解释:把 dispatch 这个方法不断用高阶函数包装,最后返回一个强化过后的 dispatch。

上面的过程是由applyMiddleware方法来实现:

 function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args);
    // 中间件重新赋值 dispatch
    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)
    }
    // 调用第一层去掉,中间件提供,getState, 新的dispatch
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 再调用第二次把第二层去掉, 并将store的中 dispatch作为最内层中间件的 next
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
// 如何使用
applyMiddleware(thunk,logger)(createStore)(reducer);

接下来我们就可以实现一个简易的redux了

export default function createStore(reducer, preloadedState) {

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []

  function getState() {
    return currentState
  }

  function subscribe(listener) {
    currentListeners.push(listener)
    return function unsubscribe() {
      const index = currentListeners.indexOf(listener)
      currentListeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    currentState = currentReducer(currentState, action)
    const listeners = currentListeners 
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState
  }
}

接下就可以测试了

import { applyMiddleware, createStore } from './redux'

function logger(store){//getState ,新的dispatch
  return function(next){//store.dispatch旧的
      return function action(playload){ // action 就是 thunk中 next
          console.log('old', store.getState());
          next(playload)
          console.log('new', store.getState())
      }
  }
}
//解析过程
// let logger = store => next =>action =>{
    
// }
const initState = {
  count: 1
}
const ADD = 'ADD';
const MINUS = 'MINUS';

function reducer(state=initState, action) {
  switch (action.type) {
    case ADD:
      return { ...state, count: state.count + action.playload };
    case MINUS:
      return { ...state, count: state.count - action.playload };
    default:
      return state; 
  }
}

// thunk
function thunk(store) {
  return function(next) { // 这种情况,会走到logger中间件
    return function(action) {
      if(typeof action === 'function') {
        return action(store.dispatch, store.getState);
      }
      // 留给下一个中间件处理
      return next(action);
    }
  }
}


const addCount = (dispatch) => {
  setTimeout(()=>{
    dispatch({
      type: ADD,
      playload: 1
    })
  },300)
}

const minusCount = (dispatch) => {
  setTimeout(()=>{
    dispatch({
      type: MINUS,
      playload: 1
    })
  },300)
}
// (store.dispatch) => (thunk(logger(store.dispatch)))
const store = applyMiddleware(thunk,logger)(createStore)(reducer);
const btn1 = document.getElementById('add');
const btn2 = document.getElementById('minus');

btn1.addEventListener('click', () => {
  store.dispatch(addCount)
})

btn2.addEventListener('click', () => {
  store.dispatch(minusCount)
})

最后附上thunk和logger中间执行的示意图:

感谢大家阅读到最后哈!所有代码链接:github