这里我们讨论的中间件,是针对前端和Node的express和koa开发而言,对于严格意义上的中间件(平台与应用之间的通用服务),例如用于缓解后台高访问量的消息中间件,不是本篇的论述意图。
当我们在写业务代码的时候,无法避免有些逻辑写得又长又乱,加上时间紧可能写出来的代码可能质量更低,这也是我们寻求更好的架构设计和更好的代码设计的原因。
AOP
AOP意为面向切面编程,是在Java的Spring框架中的重点内容,其作用如下图所示
根据上图,整个响应http的过程可以看做是一条串联的管道,对于每个请求我们都想插入相同的逻辑例如数据过滤,日志统计的目的。为了不和业务逻辑混淆一块,提高代码复用性,AOP提倡从横向切面思路向管道某个位置插入一段代码逻辑,这样就实现在任何业务逻辑前后都有相同代码逻辑段,开发者只需要专注写业务逻辑,既不影响整个相应请求的过程,而且隔离了业务逻辑,实现了高内聚低耦合原则
可以说AOP对OOP进行了补充,OOP是对做同一件事情的业务逻辑封装成一个对象,但是做一件事情过程中又想做别的事情对OOP来说难以解决(比如说一个通用的对数据格式化操作,在每一个业务对象里面写显然太过冗余)。就像上图所示,如果每次写业务都需要重复编写数据过滤和日志统计的代码,违反了单一原则。
在前端,我们可以借用这种思想通过before和after函数来实现,我们来看以下代码实现
Function.prototype.before = function(fn){
var self = this;
return function(){
fn.call(this);
self.apply(this, arguments);
}
}
实现思路是被处理的函数通过闭包封装在新的函数里,在新的函数内部按照顺序执行传入的函数fn和被处理的函数。
Function.prototype.after = function(fn){
var self = this;
return function(){
self.apply(this, arguments);
fn.call(this);
}
}
举个例子,用户提交表单之前需要用户行为统计,代码应该是这样写
function report(){
console.log('上报数据');
}
function submit(){
console.log('提交数据');
}
//参考上面的代码,report先被执行
//后面这个括号用来传入submit的参数
//也就是说report函数获得了执行权,并把原来的submit函数返回
submit.before(report)();
从代码可以看出已经把统计和数据提交业务隔离起来,互不影响。
但如果在提交数据之前,需要数据验证并且一句验证结果判断是否提交,这里就需要在before函数增加一个判断
Function.prototype.before = function(fn){
var self = this;
return function(){
var res = fn.call(this);
if(res) self.apply(this, arguments);
}
}
function report(){
console.log('上报数据');
return true;
}
function validate(){
console.log('验证不通过');
//忽略处理逻辑,直接返回false
return false;
}
function submit(){
console.log('提交数据');
}
submit.before(report).before(validate);
//结果:验证不通过
AOP思想在前端分解隔离业务已经做到位了,但是却有了一串长长的链式调用出来。特别是如果before和after传入的函数是一个异步操作的时候,又需要做些patch。下面我们来看下其他框架的解决方案
express与koa的中间件
express和koa本身都是非常轻量的框架,express是集合路由和其他几个中间件合成的web开发框架,koa是express原班人马重新打造的一个更轻量的框架,所以koa中剥离了中间件,任由用户自行添加第三方中间件
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);
//整个请求过程就是先数据统计,日志统计,最后返回一个Hello World
上图运作流程图如下
从上图来看,每一个管道都是一个中间件,每个中间件通过next方法传递执行权给下一个中间件,express就是一个收集并调用各种中间件的容器。
中间件就是一个函数,通过express的use方法接收中间件,每个中间件有express传入的req,res和next参数。 如果要把请求传递给下一个中间件必须使用next方法,当调用res.send方法则此次请求结束,node直接返回结果给客户,但是如果在res.send方法之后调用next方法,整个中间件链式调用还会往下执行,因为当前hello word的函数也是一块中间件。
借用中间件
我们可以借用中间件思想来分解我们的前端业务逻辑,通过next方法层层传递给下一个业务,做到这几点首先要有一个管理中间件的对象,我们先创建一个名为Middleware的对象
function Middleware(){
this.cache = [];
}
Middleware通过数组缓存中间件,下面是next和use方法
Middleware.prototype.use = function(fn){
if(typepf 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){
//这里的ware就是每次next传入的fn
var ware = this.middlewares.shift();
//在这里传入了next参数,这个参数是绑定了this的nex
ware.call(this, this.next.bind(this));
}
}
Middleware.prototype.handleRequest = function(){
//深拷贝
this.middlewares = this.cache.map(function(fn){
return fn;
});
this.next();
}
我们用Middleware简单使用一下
var middleware = new Middleware();
//这里的next是在调用handleRequest的时候传入
middleware.use(function(next){
console.log(1);
next();
})
middleware.use(function(next){
console.log(2);
next();
})
middleware.use(function(next){
console.log(3);
})
middleware.use(function(next){
console.log(4);
next();
})
middleware.handleRequest();
//1
//2
//3
//没有输出4是因为没有调用next方法
那么此时我们修改一下使用方法
middleware.use(function(next){
console.log(1);
next();
console.log('1结束');
})
middleware.use(function(next){
console.log(2);
next();
console.log('2结束');
})
middleware.use(function(next){
console.log(3);
console.log('3结束');
})
这时候输出的顺序应该是1 -> 2 -> 3 -> 3结束 -> 2结束 ->1结束,可以看到,每一个中间件执行权利传递给下一个中间件之后又回到当前去做别的事情,注意这里可能发生一些逻辑的顺序错误。
实际应用
function validate(data, next){
//验证
console.log('validate', data);
//通过验证
next();
}
function send(data, next){
//模拟异步
setTimeout(function(){
console.log('send', data);
next();
}, 100)
}
function goTo(url, next){
//跳转
console.log('goTo', url);
}
validate和send函数都需要数据参数,目前Middleware只传递next参数,所以需要另外传递data参数,我们需要引入一个options对象来包裹这一串逻辑所需要的数据,所以我们要修改Middleware上的函数
Middleware.prototype.use = function(fn){
if(typepf 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){
var ware = this.middlewares.shift();
//加入options参数
ware.call(this, this.options, this.next.bind(this));
}
}
Middleware.prototype.handleRequest = function(){
//深拷贝
this.middlewares = this.cache.map(function(fn){
return fn;
});
this.options = options; //缓存数据
this.next();
}
业务逻辑再做响应修改
function validate(options, next){
console.log('validate', options.data);
next();
}
function send(options, next){
console.log('send', options.data);
options.url = 'www.xx.com';
next();
}
function goTo(options){
console.log('goTo', options.url);
}
var submitForm = new Middleware();
submitForm.use(validate).use(send).use(goTo);
submitForm.handleRequest({
data: {
name: 'sysuzhyupeng',
age: 24
}
})
大功告成
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/13488.html