Node.js 的中间件模式

中间件 在 Node.js 中被广泛使用,它泛指一种特定的设计模式、一系列的处理单元、过滤器和处理程序,以函数的形式存在,连接在一起,形成一个异步队列,来完成对任何数据的预处理和后处理。

它的优点在于 灵活性:使用中间件我们用极少的操作就能得到一个插件,用最简单的方法就能将新的过滤器和处理程序扩展到现有的系统上。

常规中间件模式

中间件模式中,最基础的组成部分就是 中间件管理器,我们可以用它来组织和执行中间件的函数,如图所示:

中间件.jpg

要实现中间件模式,最重要的实现细节是:

  • 可以通过调用use()函数来注册新的中间件,通常,新的中间件只能被添加到高压包带的末端,但不是严格要求这么做;
  • 当接收到需要处理的新数据时,注册的中间件在意不执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值;
  • 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的毁掉函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件。

至于怎么处理传递数据,目前没有严格的规则,一般有几种方式

  • 通过添加属性和方法来增强;
  • 使用某种处理的结果来替换 data;
  • 保证原始要处理的数据不变,永远返回新的副本作为处理的结果。

而具体的处理方式取决于 中间件管理器 的实现方式以及中间件本身要完成的任务类型。

举一个来自于 《Node.js 设计模式 第二版》 的一个为消息传递库实现 中间件管理器 的例子:

class ZmqMiddlewareManager {
    constructor(socket) {
        this.socket = socket;
        // 两个列表分别保存两类中间件函数:接受到的信息和发送的信息。
        this.inboundMiddleware = [];
        this.outboundMiddleware = [];
        socket.on('message', message => {
            this.executeMiddleware(this.inboundMiddleware, {
                data: message
            });
        });
    }
    
    send(data) {
        const message = { data };
        
        this.excuteMiddleware(this.outboundMiddleware, message, () => {
            this.socket.send(message.data);
        });
    }
    
    use(middleware) {
        if(middleware.inbound) {
            this.inboundMiddleware.push(middleware.inbound);
        }
        if(middleware.outbound) {
            this.outboundMiddleware.push(middleware.outbound);
        }
    }
    
    exucuteMiddleware(middleware, arg, finish) {
        function iterator(index) {
            if(index === middleware.length) {
                return finish && finish();
            }
            middleware[index].call(this, arg, err => {
                if(err) {
                    return console.log('There was an error: ' + err.message);
                }
                iterator.call(this, ++index);
            });
        }
        iterator.call(this, 0);
    }
}

接下来只需要创建中间件,分别在inboundoutbound中写入中间件函数,然后执行完毕调用next()就好了。比如:

const zmqm = new ZmqMiddlewareManager();

zmqm.use({
    inbound: function(message, next) {
        console.log('input message: ', message.data);
        next();
    },
    outbound: function(message, next) {
        console.log('output message: ', message.data);
        next();
    }
});

Express 所推广的 中间件 概念就与之类似,一个 Express 中间件一般是这样的:

function(req, res, next) { ... }

Koa2 中使用的中间件

前面展示的中间件模型使用回调函数实现的,但是现在有一个比较时髦的 Node.js 框架Koa2的中间件实现方式与之前描述的有一些不太相同。Koa2中的中间件模式移除了一开始使用ES2015中的生成器实现的方法,兼容了回调函数、convert后的生成器以及asyncawait

Koa2官方文档中给出了一个关于中间件的 洋葱模型,如下图所示:

koa中间件.jpg

从图中我们可以看到,先进入inbound的中间件函数在outbound中被放到了后面执行,那么究竟是为什么呢?带着这个问题我们去读一下Koa2的源码。

koa/lib/applications.js中,先看构造函数,其它的都可以不管,关键就是this.middleware,它是一个inbound队列:

constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
}

和上面一样,在Koa2中也是用use()来把中间件放入队列中:

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
        deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
        fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
}

接着我们看框架对端口监听进行了一个简单的封装:

// 封装之前 http.createServer(app.callback()).listen(...)
listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

中间件的管理关键就在于this.callback(),看一下这个方法:

callback() {
    const fn = compose(this.middleware);
    
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    const handleRequest = (req, res) => {
        const ctx = this.createContext(req, res);
        return this.handleRequest(ctx, fn);
    };
    
    return handleRequest;
}

这里的compose方法实际上是Koa2的一个核心模块koa-compose(https://github.com/koajs/compose),在这个模块中封装了中间件执行的方法:

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, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

可以看到,compose通过递归对中间件队列进行了 反序遍历,生成了一个Promise链,接下来,只需要调用Promise就可以执行中间件函数了:

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

从源码中可以发现,next()中返回的是一个Promise,所以通用的中间件写法是:

app.use((ctx, next) => {
    const start = new Date();
    return next().then(() => {
        const ms = new Date() - start;
        console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
    });
});

当然如果要用asyncawait也行:

app.use((ctx, next) => {
    const start = new Date();
    await next();
    const ms = new Date() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

由于还有很多Koa1的项目中间件是基于生成器的,需要使用koa-convert来进行平滑升级:

const convert = require('koa-convert');

app.use(convert(function *(next) {
    const start = new Date();
    yield next;
    const ms = new Date() - start;
    console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

最后,如果觉得文章有点用处的话,求求大佬点个赞!如果发现什么错漏也欢迎提出!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容