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

中间件机制,是一种插件机制用于扩展应用的功能。在前端应用广泛,比如 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 等前端框架所实现的中间件机制,通过这种插件的方式灵活扩展,可提高框架的可维护性。

相关推荐
长天一色几秒前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_23418 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河20 分钟前
CSS总结
前端·css
NiNg_1_23420 分钟前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦21 分钟前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普42 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠1 小时前
如何通过js加载css和html
javascript·css·html
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端