Web经典架构模式之——Middleware中间件模式

在这里插入图片描述
题图:by Takashi Hanai

Cebu宿务省是菲律宾人口密度最高的省,也是菲律宾最古老的城市。

一件食物商品从制造、加工到出厂上架,中间经历的过程是一件流水线操作过程。我们可以类比有一根长长的管道,管道的一头是肉质,管道的另一头是货架上的hotdog,管道中间塞满了无数的小黄人,每个小黄人只做一件事,小黄人之间做的事情可以有前后关系,也可以没有前后关系。比如Dave负责将肉质弄成粉末,Stuart负责将计量肉末的重量以符合规格,Mark负责将肉装成肠,Bob负责贴标签……。显然,Mark负责的事情依赖于Dave,所以Mark需要处于Dave的下游,Stuart和Dave之间可以没有依赖关系,他们孰先孰后也没太大关系。理清这个关系之后,其实流水线大概也就是这样一个场景,每个小黄人就是管道的的枢纽,负责相应的工作。当然有的小黄人比较调皮,Mark将肉故意装成banana的样子,笨笨的Bob受不了banana的诱惑,一口把“banana”给吃了,导致未成品不再流转到后续的小黄人那了。

说完小黄人,我们回到今天的主题,主要来看在web中一个小而美,但是又蕴藏无穷能量的架构模式——Middleware。Middle意为中,亦有中流抵住之意。在碎片化的业务模式中,Middleware嘴角微微上扬,横枪跃马,问鼎中原。

下面我们主要从node express中间件模式,redux中间件模式这二者来看Middleware在实际业务中的使用。

express 中间件模式

我们考虑一个实际的web服务端业务模型:

在一个client端发起HTTP请求到server端的时候,我们想要做以下几件事:

记录开始时间
authentication验证用户身份。
解析cookie ,加载body
根据路由返回不同的业务处理结果
没有命中路由,页面404
服务端记录日志
计算总共花费时间并记录
处理异常其他异常
上述事件如果风格发挥不好,很容易出现代码碎片化和复制粘贴的编码风格,甚至有可能陷入callback hell回调地狱,甚至出现类似java中常见的try catch finally return模式。原因就是因为控制流和逻辑耦合到了一起。

通常优化思路无非朝着中心化控制流、解耦处理模块、代码声明让服务可配置化这三方面去走,但在express控制流中,这个事件流处理起来就如同砍瓜切菜。

const express = require(‘express’);
const app = express();
app.use(recordTime);
app.use(authentication);
app.use(cookieParser);
app.use(logger);

recordTime、authentication…这些小黄人挨个就位放入app.use中,就可以流式地依次处理一个http请求,如同抽丝剥茧。

在express中具体的中间件函数形如:

const recordTime = (req, res, next) => {

next();
}
这个模式包含了一套声明式的路由规则,和 middleware 函数上的 next 签名,共同构成了整个中间件模式的控制流。其核心构成不是权限,解析等中间件逻辑,而是路由判断,next和中断响应(验证失败、解析失败。作为中间件执行控制,解耦了具体的处理逻辑,使得更容易写出通用的细粒度的中间件。

express 4.0

express 4.0提供了非常强大的功能 Router。Router 拓展了链式决策变成树形决策,可以支持更加大型的项目。

三段式:

/**

  • app.js
    /
    import router from ‘./routes’;
    app.use(router);
    /
    *
  • router.js
  • router根index文件将当前目录下所有router收集起来给app
    /
    import fs from ‘fs’;
    import path from ‘path’;
    import express from ‘express’;
    const router = express.Router();
    fs.readdirSync(__dirname)
    .filter(filename => filename.indexOf(’.js’) > 0 && filename !== path.basename(__filename))
    .forEach(filename => {
    const subRouter = require(./${filename});
    router.use(subRouter);
    });
    export default router;
    /
    *
  • router目录下某一子路由文件
    */
    import express from ‘express’;
    const router = express.Router();
    router.get(’/’, (req, res) => {
    res.render(‘html’, {
    root: ‘…’
    });
    });
    export default router;
    当然服务层除了express框架之外,还有新秀koa的洋葱式中间件模型,中间件返回promise,后续对koa专题会进一步讲解。
    redux 中间件模式

redux作为MVVM架构中数据管理的宠儿,短小精干,极其精炼,总体积3kb。伴随React的横空出世,redux在兵器谱上赫赫有名。不过今天我们单说redux中的middleware。

行走江湖,讲求的就是一个“稳”字,redux的middleware也不例外。

export default store => next => action => {

}
redux middleware 设计成函数式编程中的curry柯里化函数

函数curry化:一种使用匿名单参数函数来实现多参数函数的方法。

柯里化的 middleware 结构好处在于:

易串联,柯里化函数具有延迟执行的特性,通过不断柯里化形成的 middleware 可以累积参数,配合组合compose的方式,很容易形成 pipeline 来处理数据流。
共享store,在 applyMiddleware 执行过程中,store 还是旧的,但是因为闭包的存在,applyMiddleware 完成后,所有的 middlewares 内部拿到的 store 是最新且相同的。
故:applyMiddleware 会对 middleware 进行层层调用,动态地对 store 和 next 参数赋值。

我们可以看下不到20多行的applyMiddleware源码:

export default function applyMiddleware(…middlewares) {
return (createStore) => (reducer, initialState, enhancer) => {
var store = createStore(reducer, initialState, enhancer)
var dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(…chain)(store.dispatch)

return {
  ...store,
  dispatch
}

}
}
middleware在store初始化时的插入形式:

createStore(
rootReducer,
initialState,
applyMiddleware(thunk, auth, serverApi, Logger)
);
下面我们举一个典型的业务场景:

即上面的applyMiddleware(thunk, auth, serverApi, Logger):

我们在日常的前后端api交互中,避免不了的要向后端post token,但是要拿到token就需要先登录。在页面mount挂载的过程中,我们就希望可以异步地向后端post拉取数据,这个时候没有token,且不想先登录然后在登录的回调中再发送请求,将该场景中业务代码中剥离出来,怎么办呢?

那么我们不妨将authentication和server api post借助中间件来处理。

我们在auth中间件中将那些“冲突”的(需要token进行SERVER_API的)请求cache下来,然后待登录成功后再handleClashActions挨个将cache住的action通过next释放到下一个中间件。如果是没有冲突的请求(即client端已经拿到token了),那么自然就放行。

export default store => next => action => {
const { auth } = store.getState();
const _auth = action[AUTH];
if (!_auth && (auth.user.token || action.NO_AUTH)) {
return next(action);
}

const serverAPI = action[SERVER_API];
if (typeof _auth === ‘undefined’ || typeof serverAPI !== ‘undefined’) {
clashedActions.push(action);
}

return doLogin(next).then(action => {
handleClashActions(next);
return action;
});
然后在server api中间件中,通过store.getState()解构出已经存储在store里的token,再进行下一步callServerApi的调用。

形如:

export default store => next => action => {
const serverAPI = action[SERVER_API];
if (typeof serverAPI === ‘undefined’) {
return next(action);
}


const { auth } = store.getState();
const _params = {
…params,
token: auth.user.token
};
next(…);
return callServerApi(…)
.then(…)
.catch(…);
};
那些传说中的古老总是藏着待去发掘的秘密,小而密的地方往往蕴含着无穷的力量,仅以此句来回应题图。

文章转载 “糕手出招”

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值