题图: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(…);
};
那些传说中的古老总是藏着待去发掘的秘密,小而密的地方往往蕴含着无穷的力量,仅以此句来回应题图。
文章转载 “糕手出招”