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