Nodejs Express 中间件三部曲之二:Middleware 中间件的原理及实现

1,485 阅读3分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

上一篇文章,我们介绍了Express Middleware 中间件的使用和分类,本章我们探讨一下中间件的实现和原理。

神奇的中间件

image.png

曾经我也很好奇, express 的 next 函数为什么这么神奇,能够通过 next 串联起一序列的函数,达到上图的效果。

var middleware = function (req, res, next) { 
    // TODO
    next(); 
    
    doSomeOthering();
}

更好奇的多个中间件串行,doSomeOthering 会有怎么样的执行效果。于是做了以下实验。

var express = require('express');

var app = express();

app.listen(3000, function () {
    console.log('listen 3000...');
});

function middlewareA(req, res, next) {
    console.log('middlewareA before next()');
    next();
    console.log('middlewareA after next()');
}

function middlewareB(req, res, next) {
    console.log('middlewareB before next()');
    next();
    console.log('middlewareB after next()');
}

function middlewareC(req, res, next) {
    console.log('middlewareC before next()');
    next();
    console.log('middlewareC after next()');
}

app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

启动服务后,输出的效果如下所示:

middlewareA before next()
middlewareB before next()
middlewareC before next()
middlewareC after next()
middlewareB after next()
middlewareA after next()

可以看到最先的中间件 middlewareA 内部的 console.log('middlewareA after next()'); 最后执行。

执行顺序大抵如此 开始执行middlewareA -> 开始执行middlewareB -> 开始执行middlewareC -> 结束执行 middlewareC -> 结束执行middlewareB -> 结束执行 middlewareA

后面认真思考了一下,有点像数据结构中的队列,每个函数都存储进队列,然后先进先出依次执行即可。唯一区别是 函数之间是有嵌套关系的。

下面我们一步步自己实现一个简单版本的中间件。

第一步:搭建 node 服务

var http = require('http');

function express() {

    var app = function (req, res) {
       res.end('200');
       return;
    }
    
    return app;    // 返回实例
}

var app = express();


http.createServer(app).listen(1338, '127.0.0.1');

console.log('Server running at http://127.0.0.1:1338/');

这是个极度简单的node服务,对于任何请求都将返回 200。

第二步:完善加上 use 函数

function express() {
    var queue = []; // 待执行的函数队列
    
    var app = function (req, res) {
       res.end('200');
       return;
    }
   
    app.use = function (task) {
        queue.push(task); // 往函数队列推入元素
    }
    
    return app;  // 返回实例
}

这样就可以通过类似 app.use(middlwareA) 的语法给绑定中间件。

第三步:消费中间件

function express() {
    var queue = []; // 待执行的函数队列

    var app = function (req, res) {
        var funcs = [...queue]; // copy 一份原始队列出来

        function next() {
            var task = funcs.shift(); // 取出最先进入队列的函数,队列元素减1
            if (!task) { // 没有的话,就执行兜底逻辑,说明都执行完了
                res.end('200');
                return;
            }
            task(req, res, next); // 执行当前函数
        }

        next();
    }

    app.use = function (task) {
        queue.push(task);
    }
    
    return app;    // 返回实例
}

第四步:测试

function middlewareA(req, res, next) {
    console.log('middlewareA before next()');
    next();
    console.log('middlewareA after next()');
}

function middlewareB(req, res, next) {
    console.log('middlewareB before next()');
    next();
    console.log('middlewareB after next()');
}

function middlewareC(req, res, next) {
    console.log('middlewareC before next()');
    next();
    console.log('middlewareC after next()');
}

app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

效果和 express 是一致的。

更多的思考

上一篇文章Nodejs Express 中间件三部曲之一:Middleware 中间件的使用和分类也提了中间件分为应用级别、路由级别等等。路由级别的使用如下:

const router = express.Router()

router.get("/user/:id",(req,res,next)=>{
    console.log('Request URL:', req.originalUrl)
    next()
},(req,res,next)=>{
    console.log('Request Type:', req.method)
    next()
},(req,res)=>{
    res.json({
        status:true,
        id:req.params.id
    })
})

或者

app.use('/', middlewareA)

可以看到它和上述简版的实现有以下几个升级。

  • 有了路由的概念
  • 支持方法匹配,router.get
  • 支持第一个参数是字符串url路径,以更精确的匹配
  • 支持一连串连续的中间件绑定
  • use 方案也支持第一个变量是字符串url路径

所以实际实现中,还要加上更多的逻辑。其主要思路如下:

  • 函数队列和路由及请求方法绑定,除了全局的函数队列外,还需要为每个 url_path + request_method 的组合分配一个函数队列,以做到更精准的匹配
  • check 函数的形参,如果第一个变量是字符串,则默认后续的形参才是中间件

参考致谢

欢迎阅读到最后,若有帮助请点赞。如果大家有兴趣的话,欢迎留言,有空继续实现一个更完整版本的中间件。以下文章给我很大参考,感谢。