Node.js 的中间件模式

作者: 你的肖同学 | 来源:发表于2018-06-11 17:19 被阅读32次

    中间件 在 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`);
    }));
    

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

    相关文章

      网友评论

        本文标题:Node.js 的中间件模式

        本文链接:https://www.haomeiwen.com/subject/awvheftx.html