前端眼中的中间件

3,369 阅读5分钟

一、引言

在web应用开发过程中,经常会听到关于中间件话题的讨论;尤其是在后端开发中,什么Dubbo(RPC框架)、Kafka(消息队列)等专业名词,简直是不知所云。随着Nodejs的蓬勃发展,中间件概念也进入了前端开发领域;那么前端和后端都在谈论中间件,他们所讲的是一个东西吗?答案是肯定的,不是同一个东西,甚至是千差万别。

后端所讲的中间件其实也没有一个很标准的定义,但普遍认为中间件是一种独立的系统软件或服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源,中间件位于客户机服务器的操作系统之上,管理计算资源和网络通信。 而前端的中间件更像是一种设计模式,它提供了一种插件机制用于增强程序的功能,称为中间件模式会更为贴切。

在前端侧讨论中间件的时候,肯定是绕不开Connect、Express、Koa这三个知名框架的,前端开发者最早接触中间件概念基本上是从Connect框架开始的,再经过Express、Koa的进一步发展和沉淀形成了大家都所熟悉的洋葱模型中间件。接下来以Koa框架为例,一起学习下前端中间件。

二、从Demo开始

const Koa = require('koa');
const app = new Koa();

/// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

/// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

/// response 
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

上面的示例非常简单,却完整的揭示了中间件使用的三个环节:声明中间件、注册中间件、执行中间件;app.use()方法负责中间件的注册,其参数部分是中间件的声明,在此可看到中间件就是个函数,通过对app.use()方法类型的跟踪,得到了具体的函数签名,如下:

// @types/koa-compose/index.d.ts
declare namespace compose {
    type Middleware<T> = (context: T, next: Koa.Next) => any; // 中间件的类型声明
    type ComposedMiddleware<T> = (context: T, next?: Koa.Next) => Promise<void>;
}
 // @types/koa/index.d.ts => Koa.Next
type Next = () => Promise<any>; // 下一个中间件

最后通过app.listen()方法(调用中间件执行方法)执行所有注册的中间件。从示例上可以看出,中间件的注册和执行都很简单(基本不用关心),中间件的声明实现才是重中之重;在使用Koa开发时正是通过引入各种不同的中间件来增强程序自身的。

三、中间件函数

根据中间件函数的签名,可得到中间件函数有两种不同的表达方式:异步函数(async function)、普通函数(common function),示例如下:

// 异步函数
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});
// 普通函数
app.use((ctx, next) => {
  const start = Date.now();
  next().then(()=>{
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
  });
});

上面两种中间件函数的运行行为和结果是一致的,因异步函数代码可读性更好些,所以更倾向使用异步函数开发中间件;如因node版本兼容问题不支持async await关键字,可采用普通函数的方式开发。

四、中间件执行顺序

中间件模式中最妙的一处就是中间件的执行顺序,理解了中间件的执行顺序也就懂得了什么是洋葱模型,下面用一个无用的示例来解释下中间件的执行顺序:

// Koa使用koa-compose模块整合中间件并提供中间件执行方法
const compose = require('koa-compose');
const middlewares = [];
const arr = [];

/// [M1]
middlewares.push(async (ctx, next) => {
  arr.push('M1-beforeNext');
  await next();
  arr.push('M1-afterNext');
});

/// [M2]
middlewares.push(async (ctx, next) => {
  arr.push('M2-beforeNext');
  await next();
  arr.push('M2-afterNext');
});

/// [M3]
middlewares.push(async (ctx, next) => {
  arr.push('M3-beforeNext');
  await next();
  arr.push('M3-afterNext');
});

compose(middlewares)().then(()=>{
  console.log(arr);
});

/* 打印结果

[
  'M1-beforeNext',
  'M2-beforeNext',
  'M3-beforeNext',
  'M3-afterNext',
  'M2-afterNext',
  'M1-afterNext'
]
*/

根据上述程序的运行结果,可以绘制出这样一个中间件执行顺序图示:

按照图示分析,首先执行M1中间件的beforeNext部分,接着进入(next)M2中间件执行其beforeNext部分,再接着进入(next)M3中间件执行其beforeNext部分;M3中间件的beforeNext部分执行完毕后,执行顺序开始回流,依次的执行afterNext部分。

Koa框架就是利用了中间件的这种执行顺序,把Request和Response的操作整合到了一个中间件中处理,来再体会下洋葱模型的图例:

结合上面中间件执行顺序图示看,是不是更加具体和形象了呢。

五、中间件模式核心

Koa的基础结构其实很简单,它就是利用了中间件模式通过载入各种不同的中间件构建起了处理web应用的能力,下面来看看中间件模式的核心方法compose,即koa-compose模块:

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      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)
      }
    }
  }
}

compose方法整合了所有的中间件并返回一个中间件执行方法供外部应用调用,就是如此的简单又如此的威力巨大。

六、中间件模式的应用

目前中间件模式经常会被应用到软件设计中,如umi-request,就是应用中间件模式的典型,通过中间件不断增强自身的处理能力;大家也可以考虑如何利用这种模式解决架构层面的一些问题。

七、参考资料