zoukankan      html  css  js  c++  java
  • .9-浅析express源码之请求处理流程(2)

      上节漏了几个地方没有讲。

    1、process_params

    2、trim_prefix

    3、done

      分别是动态路由,深层路由与最终回调。

      这节就只讲这三个地方,案例还是express-generator,不过请求的方式更为复杂。

    process_params

      在讲这个函数之前,需要先进一下path-to-regexp模块,里面对字符串的正则化有这么一行replace:

    path = ('^' + path + (strict ? '' : path[path.length - 1] === '/' ? '?' : '/?'))
    // .replace...
    .replace(/(\/)?(\.)?:(w+)((.*?))?(*)?(?)?/g, function (match, slash, format, key, capture, star, optional, offset) {
        // ...
        keys.push({
            name: key,
            optional: !!optional,
            offset: offset + extraOffset
        });
        // ...
    });

      这里会对path里面的:(...)进行匹配,然后获冒号后面的字符串,然后作为key传入keys数组,而这个keys数组是layer的属性,后面要用。

      另外还要看一个地方,就是layer.mtach,在上一节,由于传的是根路径,所以直接从fast_slash跳出了。

      如果是正常的带参数路径,执行过程如下:

    /**
     * @example path = /users/params
     * @example router.get('/users/:id')
     */
    Layer.prototype.match = function match(path) {
        var match
    
        if (path != null) {
            // ...快速匹配
    
            match = this.regexp.exec(path)
        }
    
        if (!match) { /*...*/ }
    
        // 缓存params
        this.params = {};
        this.path = match[0]
    
        // [{ name: prarms,... }]
        var keys = this.keys;
        var params = this.params;
    
        for (var i = 1; i < match.length; i++) {
            var key = keys[i - 1];
            var prop = key.name;
            // decodeURIComponent(val)
            var val = decode_param(match[i]);
            // layer.params.id = params
            if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
                params[prop] = val;
            }
        }
    
        return true;
    };

      根据注释的案例,可以看出路由参数的匹配过程,这里仅仅以单参数为例。

      下面可以进入process_params方法了,分两步讲:

    proto.process_params = function process_params(layer, called, req, res, done) {
        var params = this.params;
    
        // 获取keys数组
        var keys = layer.keys;
    
        if (!keys || keys.length === 0) return done();
    
        var i = 0;
        var name;
        var paramIndex = 0;
        var key;
        var paramVal;
        var paramCallbacks;
        var paramCalled;
    
        function param(err) {
            if (err) return done(err);
            if (i >= keys.length) return done();
    
            paramIndex = 0;
            key = keys[i++];
            name = key.name;
            // req.params = layer.params
            paramVal = req.params[name];
            // 后面讨论
            paramCallbacks = params[name];
            // 初始为空对象
            paramCalled = called[name];
    
            if (paramVal === undefined || !paramCallbacks) return param();
    
            // param previously called with same value or error occurred
            if (paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error !== 'route'))) {
                // error...
            }
    
            // 设置值
            called[name] = paramCalled = {
                error: null,
                match: paramVal,
                value: paramVal
            };
    
            paramCallback();
        }
    
        // single param callbacks
        function paramCallback(err) {
            //...
        }
    
        param();
    };

      这里除去遍历参数,有几个变量,稍微解释下:

    1、paramVal => 请求路径带的路由参数

    2、paramCallbacks => 调用router.params会填充该对象,请求带有指定路由参数会触发的回调函数

    3、paramCalled => 一个标记对象

      当参数匹配之后,会调用回调函数paramCallback:

    function paramCallback(err) {
        // 依次取出callback数组的fn
        var fn = paramCallbacks[paramIndex++];
    
        // 标记val
        paramCalled.value = req.params[key.name];
    
        if (err) {
            // store error
            paramCalled.error = err;
            param(err);
            return;
        }
    
        if (!fn) return param();
        // 调用回调函数
        try {
            fn(req, res, paramCallback, paramVal, key.name);
        } catch (e) {
            paramCallback(e);
        }
    }

      仅仅只是调用在param方法中预先填充的函数。用法参见官方文档的示例:

    router.param('user', function(req, res, next, id) {
        // ...do something
        next();
    })

      每当路由参数是user时,就会触发调用后面注入的函数,其中4个参数可以跟上面源码的形参对应。虽然源码提供了5个参数,但是示例只有4个。

    trim_prefix

      这个就比较简单了。

      案例还是按照上一节的,假设有这样的请求:

    // app.js
    app.use('/user',userRouter);
    // userRouter.js
    router.get('/abcd',()=>{...});
    // client的get请求
    path => '/users/abcd'

      此时,内部路由将其分发给了usersRouter,但是在分发之前有一个问题。

      在自定义的路由中,是不需要指定根路径的,因为在app.use中已经写明了,如果将完整的路径传递进去,在路径正则匹配时会失败,这时候就需要进行trim_prefix了。

      源码如下:

    /**
     * 
     * @param   layer       匹配到的layer
     * @param   layerError  error
     * @param   layerPath   layer.path => '/users'
     * @param   path        req.url.pathname => '/users/abcd'
     */
    function trim_prefix(layer, layerError, layerPath, path) {
        if (layerPath.length !== 0) {
            // 保证路径后面的字符串合法
            var c = path[layerPath.length]
            if (c && c !== '/' && c !== '.') return next(layerError)
    
            debug('trim prefix (%s) from url %s', layerPath, req.url);
            // 缓存被移除的path
            removed = layerPath;
            req.url = protohost + req.url.substr(protohost.length + removed.length);
    
            // 保证移除后的路径以/开头
            if (!protohost && req.url[0] !== '/') {
                req.url = '/' + req.url;
                slashAdded = true;
            }
    
            // 基本路径拼接
            req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' ?
                removed.substring(0, removed.length - 1) :
                removed);
        }
    
        debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
    
        // 将新的req.url传进去处理
        if (layerError) {
            layer.handle_error(layerError, req, res, next);
        } else {
            layer.handle_request(req, res, next);
        }
    }

      可以看出,源码就是去掉路径的头,然后将新的路径传到二级layer对象中做匹配。

    done

      这个最终回调麻烦的要死。

      注意:如果调用了res.send()后,源码内部会调用res.end结束响应,回调将不会被执行,这是为了防止意外情况所做的保险工作。

      一层一层的来看最终回调的结构,首先是handle方法中的直接定义:

    var done = restore(out, req, 'baseUrl', 'next', 'params');

      从方法名可以看出这就是一个值恢复的函数:

    function restore(fn, obj) {
      var props = new Array(arguments.length - 2);
      var vals = new Array(arguments.length - 2);
    
      // 在请求到来的时候先缓存原始信息
      /**
       * props = ['baseUrl', 'next', 'params']
       * vals = ['url','next方法','动态路由的params']
       */
      for (var i = 0; i < props.length; i++) {
        props[i] = arguments[i + 2];
        vals[i] = obj[props[i]];
      }
    
      return function () {
        // 在请求处理完后对值进行回滚
        for (var i = 0; i < props.length; i++) {
          obj[props[i]] = vals[i];
        }
    
        return fn.apply(this, arguments);
      };
    }

      简单。

      下面来看看这个fn是个啥玩意,默认情况下来源于一个工具:

    var done = callback || finalhandler(req, res, {
      env: this.get('env'),
      onerror: logerror.bind(this)
    });
    function finalhandler(req, res, options) {
      // 获取配置参数
      var opts = options || {}
      var env = opts.env || process.env.NODE_ENV || 'development'
      var onerror = opts.onerror
    
      return function (err) {
        // ...
      }
    }

      在获取参数后,返回了一个新函数,简单看一下done的调用地方:

    // 遇到router标记直接调用done
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }
    
    // 走完了layer匹配
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }
    
    // path为null
    var path = getPathname(req);
    
    if (path == null) {
      return done(layerError);
    }

      基本上正常情况下就是null,错误情况下会传了一个err,基本上符合node的err first模式。

      进入finalhandler方法:

    function done(err) {
      var headers
      var msg
      var status
    
      // 请求已发送的情况
      if (!err && headersSent(res)) {
        debug('cannot 404 after headers sent')
        return
      }
    
      // unhandled error
      if (err) {
        // ...
      } else {
        // not found
        status = 404
        msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
      }
    
      debug('default %s', status)
    
      // 处理错误
      if (err && onerror) {
        defer(onerror, err, req, res)
      }
    
      // 请求已发送销毁req的socket实例
      if (headersSent(res)) {
        debug('cannot %d after headers sent', status)
        req.socket.destroy()
        return
      }
    
      // 发送请求
      send(req, res, status, headers, msg)
    }

      原来这里才是响应的实际地点,在保证无错误并且响应未手动提前发送的情况下,调用本地方法发送请求。

      这里的send过程十分繁杂,暂时不想深究,直接看最终的发送代码:

    function write () {
      // response body
      var body = createHtmlDocument(message)
    
      // response status
      res.statusCode = status
      res.statusMessage = statuses[status]
    
      // response headers
      setHeaders(res, headers)
    
      // security headers
      res.setHeader('Content-Security-Policy', "default-src 'self'")
      res.setHeader('X-Content-Type-Options', 'nosniff')
    
      // standard headers
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
      res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
    
      // 只请求页面的首部
      if (req.method === 'HEAD') {
        res.end()
        return
      }
    
      res.end(body, 'utf8')
    }

      因为注释都解释的很明白了,所以这里简单的贴一下代码,最终调用的是node的原生res.end进行响应。

      至此,基本上完事了。

  • 相关阅读:
    开发者看过来,哪个移动平台好赚钱?
    EGit下配置Github项目
    用户接口(UI)设计的 20 条原则
    要想工作效率高,我们到底需要多少睡眠?
    Android 读取<metadata>元素的数据
    Android实现推送方式解决方案
    余晟:做个懂产品的程序员
    Gson简要使用笔记
    编程从业五年的十四条经验,句句朴实
    程序员不是包身工
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8945483.html
Copyright © 2011-2022 走看看