• .8-浅析express源码之请求处理流程(1)


      这一节就讲从一个请求到来,express内部是如何将其转交给合适的路由,路由又是如何调用中间件的。

      以express-generator为例,关键代码如下:

    // app.js
    app.use('/', indexRouter);
    app.use('/users', usersRouter);
    // indexRouter
    router.get('/', function(req, res, next) {
      console.log('first middleware');
      next();
    },(req,res,next)=>{
      res.render('index', { title: 'Express' });
    });
    // usersRouter
    router.get('/', function(req, res, next) {
      res.send('respond with a resource');
    });

      在两个路由的JS中,两次router.get调用会分别生成2个path层级的layer对象,中间件函数为内部方法route.dispatch,push进了router的stack数组中,并挂载了2个route对象。而这两个route对象根据后面的中间件函数数量又独立生成了对应的内部layer,仅处理中间件函数,同时push到了route的stack中。

      在最外层的app.js中,调用app.use,传入挂载路径与返回的router对象,由于router对象没有set方法,不是express应用,所以直接走的router.use方法。在use方法里,生成了两个Layer对象,路径为app.use的第一个参数,fn为返回的router函数对象。

      最最后,2个Layer对象会被push进app的内部独立router对象中。示意图如下:

      提前简单说一下涉及的四个模块app、router、layer、route。

    1、app => 主要负责全局配置参数读取,所有的方法最终都会指向后面的工具模块,本身不做事

    2、router => 所有app应用内部会有一个默认的router对象,该router对象上stack数组中的Layer主要根据路径把请求分发给处理对应路径的自定义router。而自定义的router上layer对象也不会直接处理请求,而是再次根据路径把请求分发给对应的route对象。route对象会遍历stack数组,依次取出layer调用中间件处理请求。

    3、layer => 请求分发对象,虽然说3个参数分别为路径、配置参数、处理函数,但是在实际情况中只会单独处理一件事。

    4、route => 最底层的对象,负责处理请求。

      这时候,假设有一个'/'根路径的get请求过来了。

    app.handle

      入口函数就是第一节就见过,但是一直没有管的app.hanle:

    function createApplication() {
        var app = function(req, res, next) {
            app.handle(req, res, next);
        };
        // mixin && init
        return app;
    }
    app.handle = function handle(req, res, callback) {
        var router = this._router;
        // 单app应用时为默认的finalhandler
        var done = callback || finalhandler(req, res, {
            env: this.get('env'),
            onerror: logerror.bind(this)
        });
    
        // no routes
        if (!router) {
            debug('no routes defined on app');
            done();
            return;
        }
    
        router.handle(req, res, done);
    };

      这里假设只有一个app应用,请求进来后封装了一个默认的callback,然后调用了router模块的handle方法。

    router.handle

      这个函数太长了,分几段来说吧。

    proto.handle = function handle(req, res, out) {
        var self = this;
    
        debug('dispatching %s %s', req.method, req.url);
    
        var idx = 0;
        // 获取请求地址的protocol + host
        var protohost = getProtohost(req.url) || ''
        var removed = '';
        // 标记斜杠
        var slashAdded = false;
        var paramcalled = {};
    
        // 应付OPTIONS方式的请求
        var options = [];
    
        // 获取本地的layer数组
        var stack = self.stack;
    
        // manage inter-router variables
        var parentParams = req.params;
        var parentUrl = req.baseUrl || '';
        var done = restore(out, req, 'baseUrl', 'next', 'params');
    
        // 挂载next方法
        req.next = next;
    
        // options请求的默认返回
        if (req.method === 'OPTIONS') {
            done = wrap(done, function(old, err) {
                if (err || options.length === 0) return old(err);
                sendOptionsResponse(res, options, old);
            });
        }
    
        // setup basic req values
        req.baseUrl = parentUrl;
        req.originalUrl = req.originalUrl || req.url;
    //
    next()... }

      函数在最开始还是整理参数,这里的restore没看懂具体作用,暂时跳过这里。

      总结来说第一部分做了以下事情:

    1、获取协议+基本地址的字符串

    2、获取stack数组,里面装的是layer对象

    3、定义标记变量

    4、对OPTIONS请求做特殊处理

    5、done方法是所有layer跑完后的最终回调,此时需要还原url

      对于OPTIONS方式的请求,若没有做特殊处理,则会返回一个默认的响应。而在servlet中,则有一个特殊的doOptions的方法专门来设置Allow请求头响应,感觉差不多。

      接下来调用一个next方法,该方法会被挂载到req上面,这是第一次调用:

    proto.handle = function handle(req, res, out) {
        // ...
        next();
        function next(err) {
            // next('route')不会被当成错误
            var layerError = err === 'route' ?
                null :
                err;
    
            // 去掉斜杠
            if (slashAdded) {
                req.url = req.url.substr(1);
                slashAdded = false;
            }
    
            // 还原被更改的req.url
            if (removed.length !== 0) {
                req.baseUrl = parentUrl;
                req.url = protohost + removed + req.url.substr(protohost.length);
                removed = '';
            }
    
            // 退出路由的信号
            if (layerError === 'router') {
                setImmediate(done, null)
                return
            }
    
            // 所有的layer都遍历完毕
            if (idx >= stack.length) {
                setImmediate(done, layerError);
                return;
            }
    
            // 获取请求的pathname
            var path = getPathname(req);
    
            if (path == null) {
                return done(layerError);
            }
    
            // 寻找下一个匹配的layer
            var layer;
            var match;
            var route;
            
            // ...more code
        }
        // ...
    }

      这一部分主要做了下列事情:

    1、判断是否有err定义layerError变量,其中next('route')会被忽略

    2、根据slashAdded变量决定是否需要切割一下url,还原完整的url(二级路由匹配)

    3、除了route,router字符串似乎在next中也有特殊意义?

      下面开始真正的匹配layer,如下:

    while (match !== true && idx < stack.length) {
        // 取出一个layer
        layer = stack[idx++];
        // 检测layer是否匹配该路径
        match = matchLayer(layer, path);
        route = layer.route;
    
        // ...
    }

      这里涉及到了Layer对象的原型方法,matchLayer(layer, path)实际上就是layer.match(path)。

      以假设条件看一下match的匹配过程:

    // app.use('/',indexRouter)满足fast_slash条件
    Layer.prototype.match = function match(path) {
        var match
    
        if (path != null) {
            // layer匹配路径为/时 匹配所有
            if (this.regexp.fast_slash) {
                this.params = {}
                this.path = ''
                return true
            }
    
            // layer匹配路径为*时 匹配所有:param
            // 调用decodeURIComponent转义path
            if (this.regexp.fast_star) {
                this.params = { '0': decode_param(path) }
                this.path = path
                return true
            }
    
            // 用生成的正则解析
            match = this.regexp.exec(path)
        }
        // 路径不匹配 返回false
        if (!match) {
            this.params = undefined;
            this.path = undefined;
            return false;
        }
    
        // 其余情况下匹配的路径
        // 后面讨论...
    
        return true;
    }

      由于假设请求路径为'/',所以这里会跳过match阶段,直接返回true。

      继续看代码:

    while (match !== true && idx < stack.length) {
        // 取出一个layer
        layer = stack[idx++];
        match = matchLayer(layer, path);
        route = layer.route;
        // 报错
        if (typeof match !== 'boolean') layerError = layerError || match;
        // Layer未匹配
        if (match !== true) continue;
        // app内部router对象的layer不存在route
        if (!route) continue;
        // 处理错误
        if (layerError) {
            // routes do not match with a pending error
            match = false;
            continue;
        }
    
        // ...处理外部router对象上的layer
    }

      需要注意的是,这里的匹配是对app的内部路由上的Layer进行遍历,而这些layer是没有route对象挂载的,仅仅是用来分发外部路由,因此这里会continue直接跳过后面的流程。

      由于已经匹配到对应的Layer,所以while循环跳出,继续下面的流程:

    // 根据配置参数处理参数合并
    req.params = self.mergeParams ?
        mergeParams(layer.params, parentParams) :
        layer.params;
    // 获取layer匹配的path => ''
    var layerPath = layer.path;
    
    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function(err) {
        // ...trim_prefix(layer, layerError, layerPath, path)
    });

      在生成路由会有一个合并参数的选项,决定是否将父路由的参数合并到子路由,默认为false。

      接下来获取layer匹配的path后,调用了另外一个方法,而这个方法主要是处理/path:prarms这种形式的参数,所以跳过。

      而回调的trim_prefix函数内容也直接跳过,后面讲,直接进入layer.handle_request函数:

    Layer.prototype.handle_request = function handle(req, res, next) {
        // 这里的handle是router函数对象
        var fn = this.handle;
        // 错误处理中间件有4个参数
        if (fn.length > 3) {
          return next();
        }
        // 调用具体外部路由的handle方法
        try {
          fn(req, res, next);
        } catch (err) {
          next(err);
        }
    };

      这里从app的内部路由handle方法跳到了外部路由的handle中,再走一遍流程。

      由于req、res始终是一个,所以大部分的都可以跳过,这里挑不同的地方来讲:

    1、stack

      var stack = self.stack;

      由于换了router,所以stack也换成了外部路由的stack,里面装的是有route挂载的layer。

    2、while循环的后半段

    // 获取请求的方式
    var method = req.method;
    var has_method = route._handles_method(method);
    
    // OPTIONS请求特殊处理
    if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
    }
    
    // 如果route未处理该方式请求 直接跳过
    if (!has_method && method !== 'HEAD') {
        match = false;
        continue;
    }
    
    Route.prototype._handles_method = function _handles_method(method) {
        // router.all
        if (this.methods._all) {
            return true;
        }
        var name = method.toLowerCase();
        // head默认视为get请求
        if (name === 'head' && !this.methods['head']) {
            name = 'get';
        }
        // 判断route是否有处理该请求方式的中间件
        return Boolean(this.methods[name]);
    };
    
    // route[METHODS]
    var layer = Layer('/', {}, handle);
    layer.method = method;
    
    this.methods[method] = true;

      这里做了一个提前判断,在调用app[METHODS]、router[METHODS]时,最后指向底层的route[METHODS]。除了生成一个layer对象,还会同时将route的本地属性methods对象上对应方式的键设为true,表示这个route有处理对应请求方式的layer。

      在跳过process_params、trim_prefix后,还是回到了handle_request方法。

      然而,这里的layer对应的handle并不指向中间件函数,而是route.dispatch.bind(route),如下:

    // router.get('/',fn1,fn2)...
    var layer = new Layer(path, {
        sensitive: this.caseSensitive,
        strict: this.strict,
        end: true
    }, route.dispatch.bind(route));

      真正的中间件函数是在layer.route上,所以这个是另外一个分发方法,负责把对应方式的请求转给对应的route。

    Route.prototype.dispatch = function dispatch(req, res, done) {
        var idx = 0;
        var stack = this.stack;
        if (stack.length === 0) {
            return done();
        }
        // 格式化请求方式
        var method = req.method.toLowerCase();
        if (method === 'head' && !this.methods['head']) {
            method = 'get';
        }
        // 最终匹配的route
        req.route = this;
    
        next();
    
        function next(err) {
            // signal to exit route
            if (err && err === 'route') return done();
    
            // err...
            // 依次取出route对象stack中的layer
            var layer = stack[idx++];
            // err...
    
            if (err) {
                layer.handle_error(err, req, res, next);
            } else {
                // 又是这个方法
                layer.handle_request(req, res, next);
            }
        }
    };

      这个dispatch与handle方法十分类似,依次取出layer并再次调用其handle_request方法,这里的layer里面的handle是最终处理响应请求的中间件函数。

      在文档中指出,需要执行中间件的第三个参数next中间件才会继续走下去,从这里也能看出,调用next后回到dispatch方法,会从stack上取出下一个layer,然后继续执行中间件函数,直到所有的layer都过了一遍,会调用回调函数done,这个方法就是最初router.handle里面的next函数,开始下一轮读取。

      当内部路由上的layer都过完,请求就处理完毕。正常情况下,会结束响应。接下来会调用最终回调,简单看一下比较复杂,后面单独讲

      完结。

  • 相关阅读:
    实习第十天
    实习第九天
    实习第八天
    武汉第七天
    武汉第六天
    实习第五天
    实习第四天
    NSArray
    NSString
    NSObject
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8883183.html
Copyright © 2020-2023  润新知