es6时代来了,相信会让一批有java,C++等面向对象语言开发基础的伙子们,感觉到来自js世界满满的善意。es6可让开发者几乎摆脱prototype的编程模式,让开发更加如丝般顺滑,虽然目前大部分浏览器并无支持es6,可是打鸡血般日新月异的node和与时俱进的babel,仍是已经让大部分前端和node开发者享受到es6时代的酸爽。面向对象有不少精妙的设计思想,虽然说思想 js框架相信你们都用过很多了,前端如redux,后台框架如express,koa,等等等等,固然还有不少其余优秀的框架,不过与咱们今天的主题无关就很少说了。若是你们使用过redux或者koa,应该对其中的中间件不会陌生。中间件虽然在不一样的框架中用法各有不一样,可是实现原理倒是大致一致的。咱们发现做为一个中间件,无论其具体实现的是什么能力,其实它一个最主要的职能就是加强目标对象的能力。在研究各大中间件的过程当中,隐隐约约看看一个及其熟悉的背影,那就是装饰模式。在众多设计模式中,装饰模式应用最普遍的就是加强目标对象能力,大部分中间件的实现,应该都是借鉴了装饰模式这种灵活的设计思想。所以,这里咱们首先来介绍一下 装饰模式,说到装饰模式,就不得不先提一下es7提案中新增的注解功能(本人习惯叫注解,由于写法相似于java中的注解),好比以下一个类,定义了加和减两个方法:前端
class MyClass {
add(a, b){
return a + b;
}
sub(a, b){
return a - b;
}
}
复制代码
假如如今有个需求,须要实现每次调用add或者sub函数的时候,都分别打印出方法调用先后的log,好比调用前'before operate',调用后打印'after operate',咱们是否须要在调用先后分别调用console.log(),es7里面固然没必要了,咱们只须要定义好咱们须要的打印函数,而后使用@注解,好比以下使用方式:java
//注解的函数定义
let log = (type) => {
const logger = console;
return (target, name, descriptor) => {
const method = descriptor.value;
descriptor.value = (...args) => {
logger.info(`(${type}) before function execute: ${name}(${args}) = ?`);
let ret = method.apply(target, args);
logger.info(`(${type})after function execute: ${name}(${args}) => ${ret}`);
return ret;
}
}
}
//注解调用
class MyClass {
@log("add")
add(a, b){
return a + b;
}
@log("sub")
sub(a, b){
return a - b;
}
}
复制代码
如上在咱们调用MyClass实例化方法add和sub的时候,分别会打印调用前和调用后的日志了,这就是在不改动MyClass源码的状况下,使用装饰模式对于原方法add和sub的能力加强,这是es7的语法,定义注解的方式很简单,一个函数返回另外一个函数,返回函数的参数分别是target:类的上下文,name:目标方法名,descriptor就不用解释了吧,不理解能够看看defineProperty的定义,简单易用,须要加强其余能力,那就多定义几个,多@几下。这是es7的,编译器支持的仍是看着有点抽象,接下来咱们来看看普通es5对象如何使用装饰模式进行能力的加强。以下一个add函数node
function add(a, b){
return a + b;
}
复制代码
如今须要加强log和notify的能力,在调用前打印日志并发送消息。代码以下:es6
function logDecorator(target){
var old = target;
return function(){
console.log("log before operate");
var ret = old.apply(null,arguments);
console.log(target.name,"results:",ret,",log after operate");
return ret;
}
}
function notifyDecorator(target){
var old = target;
return function(){
console.log("notify before operate");
var ret = old.apply(null,arguments);
console.log("finished, notify u");
return ret;
}
}
var add = logDecorator(notifyDecorator(add));
复制代码
稍微解释一下,var old = target;先将原目标保存,并返回一个函数,在该函数中var ret = old.apply(null,arguments);
执行原目标函数的调用,这时候,或前或后,在须要的节点进行具体的能力加强便可,是否是很失望呢,咋就这么简单?很差意思,真就这么简单,这就是各大框架中高大上的中间件的基本原理了。以koa举例,若是咱们须要简单实现一个log中间件,应该怎么作呢?express
module.exports = (opts = {}) => {
var log = console.log;
return async (ctx, next) => {
log("before ",ctx.request.url, "...");
await next();
log("after ",ctx.request.url, "...");
}
}
复制代码
如上代码就是了,固然,咱们能够在中间件中作一些过滤条件,好比咱们只但愿对非静态资源的请求进行自定义的log等等。koa以及express做为一个后台框架,中间件比较不一样的地方就在于路由的实现,听起来彷佛有点复杂哦。其实,以koa为例,想要实现路由,咱们对ctx.request.url进行字符串分析处理进入不一样的处理函数,是否就能够有一个基本的路由功能了呢?因此中间件很强大,其实也很简单,它并不矛盾。中间件定义完了,接下来看看怎么用了。
咱们的中间件可能须要十个八个,那这么多个中间件们是如何进行compose呢,不一样框架实现方式可能不太一致,可是原理仍是同一个原理。一批中间件加入以后,存于一个函数列表中,而后对列表中的函数进行顺序执行,且每个函数的返回值做为下一个函数的入参。咱们以koa和redux的中间件为例来分析一下。首先来看koa的:编程
app.use(中间件);
复制代码
koa-compose源码:redux
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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
let fn = compose(middlewares);
fn(ctx)...;
复制代码
首先,使用app.use()加入中间件,使用如上compose函数对中间件middlewares列表进行递归调用。具体代码就不一一解释了吧,对于熟悉koa以及express的同窗,应该很熟悉next的用法,这其实就是咱们前面的var old = target;
这种方式的升级版本,而且经过next的方式能够更加优雅地解了中间件新增的问题,而不须要使用嵌套调用的方式。
递归遍历是个思路,其实咱们js原生提供了一种方式进行compose,能够更加优雅解决这个问题,redux就是采用了这种调用方式,就是使用reduce函数,咱们来看看redux处理方式:设计模式
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)))
}
复制代码
reduce再加上es6简直赏心悦目有没有,若是看的不太舒服能够转成es5看看,给你们一个简单的测试用例跑跑,可能会更加好理解:api
function fun1(obj){
console.log(1);
obj.a=1;
return obj;
}
function fun2(obj){
console.log(2);
obj.b=2;
return obj;
}
let fn = compose(fun1,fun2);
fun({});
复制代码
看看调用的结果是啥,这只是一个帮助理解的小栗子,栗子虽小,可是已经小秀了一把肌肉了,重点就在于咱们在各个中间件中透传传入的这个参数obj了,能够是个对象,也能够是个函数,总之是咱们能够随心所欲地加强它的能力。
根据不一样的目的,中间件的实现机制会有一些差别,koa跟redux其实就有比较明显的一些区别,有兴趣能够深刻去看看,可是万变不离其宗。
到此,中间件的定义和调用中的一些核心逻辑就讲完了,都是我的一些浅见,水平有限,若有谬误,敬请指出!!!浏览器