装饰者模式的定义: 在不改变对象自身的基础上, 在程序运行期间给对象动态地添加方法.
AOP
在前端, 可以通过 before
和 after
函数来实现:
1 2 3 4 5 6 7 8
| Function.prototype.before = function (fn) { var self = this; return function () { fn.call(this); self.apply(this, arguments); }; };
|
1 2 3 4 5 6 7 8
| Function.prototype.after = function (fn) { var self = this; return function () { self.apply(this, arguments); fn.call(this); }; };
|
实现思路是对被处理的函数通过闭包封装在新的函数里, 在新的函数内部按照顺序执行传入的参数 fn
和被处理的函数。
举个栗子: 用户提交表单数据之前需要用户行为统计, 代码应该是这样写:
1 2 3 4 5 6 7 8 9 10 11 12
| function report() { console.log('上报数据'); } function submit() { console.log('提交数据'); } submit.before(report)();
report.after(submit)();
|
从代码可以看出已经把统计和数据提交业务隔离起来, 互不影响。
但是如果提交数据之前, 需要数据验证并且依据验证结果判断是否能提交,这里要改动 before
函数:
1 2 3 4 5 6 7 8
| Function.prototype.before = function (fn) { var self = this; return function () { var res = fn.call(this); if (res) self.apply(this, arguments); }; };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function report() { console.log('上报数据'); return true; } function validate() { console.log('验证不通过'); return false; } function submit() { console.log('提交数据'); }
submit.before(report).before(validate)();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function report() { console.log('上报数据'); return true; } function validate() { console.log('验证通过'); return true; } function submit() { console.log('提交数据'); }
submit.before(report).before(validate)();
|
AOP 思想在前端分解隔离业务已经做到位了, 但是却有了一串长长的链式出来, 如果处理不当很容易让维护者看晕;
例如下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function report() { console.log('上报数据'); return true; } function validate() { console.log('验证通过'); return true; } function submit() { console.log('提交数据'); } function goTo() { console.log('返回首页'); } submit.before(report).before(validate).after(goTo)();
|
express 与 koa 的中间件
express
和 koa
本身都是非常轻量的框架, express
是集合路由和其他几个中间件合成的 web 开发框架.
koa
是express
原班人马重新打造一个更轻量的框架, 所以 koa
已经被剥离所有中间件, 甚至连 router
中间件也被抽离出来, 任由用户自行添加第三方中间件。
express
和 koa
中间件原理一样, 直接就抽 express
来说:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| var express = require('express'); var app = express();
app.use(function (req, res, next) { console.log('数据统计'); next(); });
app.use(function (req, res, next) { console.log('日志统计'); next(); });
app.get('/', function (req, res, next) { res.send('Hello World!'); });
app.listen(3000);
|
上图运作流程图如下:
从上图来看, 每一个管道都是一个中间件, 每个中间件通过next
方法传递执行权给下一个中间件, expres
s 就是一个收集并调用各种中间件的容器。
中间件就是一个函数, 通过 express
的 use 方法接收中间件, 每个中间件有 express
传入的 req
, res
和 next
参数。
如果要把请求传递给下一个中间件必须使用 next()
方法。当调用 res.send
方法则此次请求结束, node 直接返回请求给客户, 但是若在 res.send
方法之后调用 next
方法, 整个中间件链式调用还会往下执行, 因为当前 hello world 所处的函数也是一块中间件, 而 res.send
只是一个方法用于返回请求。
借用中间件
我们可以借用中间件思想来分解我们的前端业务逻辑, 通过next
方法层层传递给下一个业务。
做到这几点首先必须有个管理中间件的对象, 我们先创建一个名为 Middleware 的对象:
1 2 3
| function Middleware() { this.cache = []; }
|
Middleware通过数组缓存中间件。下面是next和use方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Middleware.prototype.use = function (fn) { if (typeof fn !== 'function') { throw 'middleware must be a function'; } this.cache.push(fn); return this; };
Middleware.prototype.next = function (fn) { if (this.middlewares && this.middlewares.length > 0) { var ware = this.middlewares.shift(); ware.call(this, this.next.bind(this)); } };
Middleware.prototype.handleRequest = function () { this.middlewares = this.cache.map(function (fn) { return fn; }); this.next(); };
|
我们用Middleware简单使用一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var middleware = new Middleware(); middleware .use(function (next) { console.log(1); next(); }) .use(function (next) { console.log(2); next(); }) .use(function (next) { console.log(3); }) .use(function (next) { console.log(4); next(); }); middleware.handleRequest();
|
4
没有出来是因为上一层中间件没有调用next
方法;
升级一下 Middleware 高级使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| middleware .use(function (next) { console.log(1); next(); console.log('1结束'); }) .use(function (next) { console.log(2); next(); console.log('2结束'); }) .use(function (next) { console.log(3); next(); console.log('3结束'); });
|
上面代码的流程图:
可以看出: 每一个中间件执行权利传递给下一个中间件并等待其结束以后又回到当前并做别的事情, 方法非常巧妙, 有这特性我们可以玩转中间件。
实际应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
function validate(data, next) { console.log('validate', data); next(); }
function send(data, next) { setTimeout(function () { console.log('send', data); next(); }, 1000); } function goTo(url, next) { console.log('goTo', url); }
|
validate 和 send 函数都需要数据参数, 目前 Middleware 只传 next
, 需要传递 data
数据才能顺利执行下去, 然而每个中间件需要的数据不一定都一致 (就像 goTo 与 validate、send )。
我们需要引入一个 options
对象来包裹这一串逻辑需要的数据, 每个中间件在 options
内提取自己所需的数据, 这样就能满足所有中间件, 函数做相应调整;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function Middleware() { this.cache = []; this.options = null; }
Middleware.prototype.use = function (fn) { if (typeof fn !== 'function') { throw 'middleware must be a function'; } this.cache.push(fn); return this; };
Middleware.prototype.next = function (fn) { if (this.middlewares && this.middlewares.length > 0) { var ware = this.middlewares.shift(); ware.call(this, this.options, this.next.bind(this)); } };
Middleware.prototype.handleRequest = function (options) { this.middlewares = this.cache.map(function (fn) { return fn; }); this.options = options; this.next(); };
|
业务逻辑做相应修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function validate(options, next) { console.log('validate', options.data); next(); } function send(options, next) { setTimeout(function () { console.log('send', options.data); options.url = 'www.baidu.com'; next(); }, 3000); } function goTo(options) { console.log('goTo', options.url); }
var submitForm = new Middleware(); submitForm.use(validate).use(send).use(goTo); submitForm.handleRequest({ data: { name: 'xiaoxiong', age: 20 } });
submitForm.handleRequest({ data: { name: 'xiaohong', age: 21 } });
|
以上代码大功告成。
总结
通过以上代码, 实现了业务隔离, 满足每个业务所需的数据, 又能很好控制业务下发执行的权利, 所以 中间件 模式算是一种不错的设计。
从代码阅读和代码编写的角度来说难度并不大, 只要维护人员拥有该方面的知识, 问题就不大了。
参考文档