深入解析前端常用的中间件机制

中间件机制,是一种插件机制用于扩展应用的功能。在前端应用广泛,比如 Koa、Redux等。

Koa

Koa的中间件机制,比较经典,被称为洋葱模型。

如图请求从外到内经过中间件处理,然后响应再从内到外处理。

下面通过例子解释一下 Koa 的中间件是怎么实现的

javascript 复制代码
const Koa = require('koa')
const app = new Koa()

app.use(async (cxt, next) => {
  console.log('middleware1 start')
  await next()
  console.log('middleware1 end')
})

app.use(async (cxt, next) => {
  console.log('middleware2 start')
  await next()
  console.log('middleware2 end')
})

app.use(async (cxt, next) => {
  console.log('middleware3')
})

app.listen(3000)


// 输出
middleware1 start
middleware2 start
middleware3
middleware2 end
middleware1 end

koa通过use注册和串联中间件,

javascript 复制代码
use(fn) {
   this.middleware.push(fn);
   return this;
}

然后在listen方法内部执行了 const fn = compose(this.middleware),将中间件进行组装,返回中间件组合函数。

compose来源于koa-compose包,源码不到50行,但非常精妙。

compose是基于Promise的流程控制方式,Promise.resolve可以确保中间件函数的执行结果是 Promise,允许中间件函数是以同步或异步方式的执行。

javascript 复制代码
function compose(middleware) {
    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 = 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);
            }
        }
    }
}
  • 在中间件中通过await next() 执行下一个中间件 ,其实就是dispatch.bind(null, i + 1))

最终的执行过程相当于:

javascript 复制代码
async function middleware1() {
  // ...
  await (async function middleware2() {
    // ...
    await (async function middleware3() {
     // ...
    });
    // ...
  });
  // ...
}

Express

javascript 复制代码
const express = require('express')
const app = express()

function middlewareA(req, res, next) {
    console.log('middlewareA before');
    next();
    console.log('middlewareA after');
}

function middlewareB(req, res, next) {
    console.log('middlewareB before');
    next();
    console.log('middlewareB after');
}


app.use(middlewareA);
app.use(middlewareB);

express 并不是一个洋葱模型,而是基于回调的嵌套调用。不如Koa优雅,调用过程类似于:

javascript 复制代码
((req, res) => {
  console.log('第一个中间件');
  ((req, res) => {
    console.log('第二个中间件');
    (async(req, res) => {
      console.log('第三个中间件 => 是一个 route 中间件,处理 /api/test1');
      await sleep(2000)
      res.status(200).send('hello')
    })(req, res)
    console.log('第二个中间件调用结束');
  })(req, res)
  console.log('第一个中间件调用结束')
})(req, res)

不考虑路由,实现简易的express:

javascript 复制代码
const http = require("http");

class Express {
  constructor() {
    this.stack = [];
  }

  use(fn) {
    this.stack.push(fn);
  }

  // 核心机制
  handle(req, res, stack) {
    const next = () => {
      const middleware = stack.shift();
      if (middleware) {
        middleware(req, res, next);
      }
    };
    next();
  }
  callback() {
    return (req, res) => {
      this.handle(req, res, this.stack);
    };
  }

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

Redux

redux中间件机制对store.dispatch方法进行扩展,在 dispatch action 和执行 reducer 之间扩展功能。

javascript 复制代码
export default function applyMiddleware(...middlewares) {
  return createStore => reducer => {
    const store = createStore(reducer);
    let dispatch = store.dispatch;

    const midApi = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    };
    // 让中间件函数传入 midApi 执行一遍
    const middlewareChain = middlewares.map(middleware => middleware(midApi));
    // 组合函数
    dispatch = compose(...middlewareChain)(store.dispatch);
    return {
      ...store,
      // 加强版的dispatch
      dispatch
    };
  };
}

// compose
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 把中间件组合成一个加强版的dispatch函数,compose将函数按照从右到左的顺序组合起来,形成一个函数管道,使得数据依次经过这些函数进行处理。

举个例子:compose [fn1,fn2,fn3] 变为 (...args) ⇒ fn1(fn2(fn3(...args)))

下面介绍一些 Redux 常用的中间件:

  • redux-logger

简单实现:

javascript 复制代码
function logger({getState}) {
  return next => action => {
    console.log(`action  ${action.type} `);
  
    const prevState = getState();
    console.log("prev state", prevState);

    const returnValue = next(action);
    const nextState = getState();
    console.log("next state", nextState); 
    return returnValue;
  };
}
  • redux-thunk

使用:

javascript 复制代码
// thunk 函数,返回一个参数为 dispatch 的函数
export function createThunkAction(payload) {
    return function(dispatch) {
        // 调用reducer
        setTimeout(() => {
           dispatch({type: 'THUNK_ACTION', payload: payload}) 
        }, 1000)
    }
}

// 组件里调用
this.dispatch(createThunkAction(payload))

源码:

javascript 复制代码
export function createThunkMiddleware(extraArgument) {
  const middleware = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      // 如果是函数就执行
      return action(dispatch, getState, extraArgument)
    }
    return next(action)
  }
  return middleware
}
  • redux-promise

源码:

javascript 复制代码
import isPromise from 'is-promise';
import { isFSA } from 'flux-standard-action';

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action) ? action.then(dispatch) : next(action);
    }

    // 判断 payload 是否是 promise
    return isPromise(action.payload)
      ? action.payload
          .then(result => dispatch({ ...action, payload: result }))
          .catch(error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          })
      : next(action);
  };
}

Umi-requset

javascript 复制代码
import request, { extend } from 'umi-request';
request.use(async (ctx, next) => {
  console.log('a1');
  await next();
  console.log('a2');
});
request.use(async (ctx, next) => {
  console.log('b1');
  await next();
  console.log('b2');
});

const data = await request('/api/v1/a');

// a1 -> b1 -> response -> b2 -> a2

内部也是compose中间件数组,compose实现和Koa的如出一辙:

javascript 复制代码
// 返回一个组合了所有插件的"插件"
export default function compose(middlewares) {
  if (!Array.isArray(middlewares)) throw new TypeError('Middlewares must be an array!');

  const middlewaresLen = middlewares.length;
  for (let i = 0; i < middlewaresLen; i++) {
    if (typeof middlewares[i] !== 'function') {
      throw new TypeError('Middleware must be componsed of function');
    }
  }

  return function wrapMiddlewares(params, next) {
    let index = -1;
    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() should not be called multiple times in one middleware!'));
      }
      index = i;
      const fn = middlewares[i] || next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(params, () => dispatch(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }

    return dispatch(0);
  };
}

总结

本文介绍了 Koa、Express、Redux 等前端框架所实现的中间件机制,通过这种插件的方式灵活扩展,可提高框架的可维护性。

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端