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