express.js

Table of Contents

1 router

1.1 index.js

1.1.1 init & proto

뭔가 기능을 사용하기 편하도록 미리 저장해놓는 초기화 코드가 있다.

또한 아래에 있는 Route, Layer 모듈을 가져온다.

var Route = require('./route');
var Layer = require('./layer');
var methods = require('methods');
var mixin = require('utils-merge');
var debug = require('debug')('express:router');
var deprecate = require('depd')('express');
var flatten = require('array-flatten');
var parseUrl = require('parseurl');
var setPrototypeOf = require('setprototypeof')

var objectRegExp = /^\[object (\S+)\]$/;
var slice = Array.prototype.slice;
var toString = Object.prototype.toString;

또한 proto는 Router 를 주어진 options 을 이용하여 초기화 한다.

/**
 * Initialize a new `Router` with the given `options`.
 * @param {Object} [options]
 * @return {Router} which is a callable function
 * @public
 */
var proto = module.exports = function(options) {
  var opts = options || {}

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // mixin Router class functions
  // `router` 함수의 프로토타입을 `proto`로 설정하여 `proto`의 메서드를 상속받는다.
  setPrototypeOf(router, proto)

  router.params = {};  // 파라미터에 저장할 객체.
  router._params = []; // 내부적으로 사용할 파라미터 배열.
  router.caseSensitive = opts.caseSensitive;  // 경로의 대소문자 구분 여부.
  router.mergeParams = opts.mergeParams;  // 부모 라우터의 파라미터와 병합 여부.
  router.strict = opts.strict;  // 경로의 끝에 슬래시가 있어야 하는지 여부.
  router.stack = []  // 미들웨어와 라우트를 저장할 배열.

  return router;
};

1.1.2 param

  1. 파라미터 플레이스홀더와 콜백 매핑:
    • 주어진 파라미터 플레이스 홀더(parameter placeholder) `name` 을 특정 콜백함수와 매핑함.
  2. 파라미터 매핑(parameter mapping)의 용도:
    • 경로에 있는 플레이스홀더 파라미터에 대해 사전 조건(pre-condition)을 제공하는데 사용됨.
    • 예를들어, `_:userid_` 파라미터는 데이터베이스에서 사용자 정보를 자동으로 로드할 수 있다.
  3. 콜백함수의 서명:
    • 미들웨어와 같은 시그니처를 가지지만, placeholder 값이 추가로 전달된다.
  4. next() 함수 호출:
    • 미들웨어와 마찬가지로 `next()` 함수를 호출하여 다음 라우트 또는 파라미터 함수로 이어진다.
    • 요청이 중단되지 않도록 요청에 응답하거나 `next()` 를 호출해야 한다.
proto.param = function param(name, fn) {
  // param logic

  if (typeof name === 'function') {
    // 경고메시지: 함수형 파라미터 사용은 권장되지 않음.
    deprecate('router.param(fn): Refactor to use path params');
    // 함수형 파라미터를 `_params`  배열에 추가.
    this._params.push(name);
    return;
  }

  // apply param functions
  var params = this._params;
  var len = params.length;
  var ret;

  // 파라미터 이름 전처리
  if (name[0] === ':') {
    deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.slice(1)) + ', fn) instead')
    // 이름에서 콜론 제거: 파라미터 이름이 콜론으로 시작하면 제거
    // 권장 사항: 콜론 없이 사용하자
    name = name.slice(1)
  }

  // 기존 파라미터 함수 적용
  for (var i = 0; i < len; ++i) {
    // _params 배열의 각 함수에 대해 파라미터 함수 적용(apply)
    // 적용 후 리턴(ret)값이 있으면 fn을 업데이트
    if (ret = params[i](name, fn)) {
      fn = ret;
    }
  }

  // ensure we end up with a
  // middleware function
  // 유효성검사 : `fn` 이 함수가 아니면 오류 발생
  if ('function' !== typeof fn) {
    throw new Error('invalid param() call for ' + name + ', got ' + fn);
  }

  // 파라미터 미들웨어 추가
  // `params` 객체에 파라미터 이름으로 미들웨어 추가 
  (this.params[name] = this.params[name] || []).push(fn);
  return this;
};
  1. 사용 사례
    1. 사용자 정보 로드

      // user_id 가 경로에 있을 때마다 미들웨어 실행
      app.param('user_id', function(req, res, next, id){
        // 유저 로드 로직
        // id를 이용해 유저 정보를 로드하고 `req.user` 에 저장.
        User.find(id, function(err, user){
          if (err) {
            return next(err);
          } else if (!user) {
            return next(new Error('failed to load user'));
          }
          req.user = user;
          next();
        });
      });
      
    2. 로그 기록: 특정 파라미터가 요청에 포함될 때마다 로그 기록

      app.param('order_id', function(req, res, next, id) {
        console.log('Order ID:', id);
        next();
      });
      
    3. 파라미터 유효성 검사

      app.param('productId', function(req, res, next id) {
        if (!isValidProductId(id)) {
          return res.status(400).send('Invalid product Id');
        }
      })
      

1.1.3 handle

  • 요청 미들웨어 및 라우트를 순차적으로 처리
  • 각 레이어와 경로를 비교하여 적절한 라우트로 디스패치
  • 오류발생시 다음 미들웨어로 오류 전달
  • 자동으로 OPTIONS 응답 생성
  • 경로 파라미터를 병합하고 처리
proto.handle = function handle(req, res, out) {
  // `self` : 현재 `Router` 인스턴스 참조.
  var self = this;

  debug('dispatching %s %s', req.method, req.url);

  // idx : 현재 스택의 인덱스
  // protohost : 프로토콜과 호스트 부분 추출
  // removed : 제거된 경로
  // slashAdded : 패스에 슬래시 추가 여부
  // sync : 동기 호출 횟수.
  // paramcalled : 파라미터 호출 상태.
  var idx = 0;
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  var slashAdded = false;
  var sync = 0
  var paramcalled = {};

  // store options for OPTIONS request
  // only used if OPTIONS request
  var options = [];

  // middleware and routes
  var stack = self.stack;

  // manage inter-router variables
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');

  // setup next layer
  req.next = next;

  // for options requests, respond with a default if nothing else responds
  // 옵션 요청 처리
  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`은 현재 라우터의 baseUrl을 설정.
  // 중첩된 라우터에서 상위 라우터의 기본 URL을 유지하는데 사용됨.
  // `parentUrl` = 상위 라우터의 `baseUrl`
  req.baseUrl = parentUrl;
  // `req.originalUrl`은 초기 요청 URL을 저장한다.
  // `req.originalUrl`이 이미 설정되어 있지 않으면, `req.url` 값을 사용한다.
  // 원래 요청 URL을 추적하여 요청 흐름 중 변경되지 않도록 한다.
  req.originalUrl = req.originalUrl || req.url;

  next();

  function next() {...}
  function trim_prefix(layer, layerError, layerPath, path) {...}
};
  1. next()

    next 는 Express.js의 요청 처리 흐름을 제어하는 핵심 함수이다. 이 함수는 미들웨어 스택에서 다음 미들웨어나 라우트로 제어를 전달한다.

    function next(err) {
      // 1. 오류 처리:
      var layerError = err === 'route' ? null : err;
    
      // remove added slash 추가된 슬래시 제거 
      if (slashAdded) {
        req.url = req.url.slice(1);
        slashAdded = false;
      }
    
      // restore altered req.url
      // `removed` 길이가 0이 아니면 원래 `baseUrl`, `url` 로 변경.
      // `removed` 는 `trim_prefix` 에서 변경을 수행함.
      if (removed.length !== 0) {
        req.baseUrl = parentUrl;
        req.url = protohost + removed + req.url.slice(protohost.length);
        removed = '';
      }
    
      // signal to exit router
      // error 타입이 'router' 면 멈춘다.
      if (layerError === 'router') {
        setImmediate(done, null);
        return;
      }
    
      // no more matching layers
      // 미들웨어 스택의 끝
      if (idx >= stack.length) {
        setImmediate(done, layerError);
        return;
      }
    
      // max sync stack
      // 동기 스택 최대 한도
      if (++sync > 100) {
        return setImmediate(next, err);
      }
    
      // get pathname of request
      // 경로명 가져오기 : getPathname을 이용하여 요청 경로명 추출.
      var path = getPathname(req);
    
      if (path == null) {
        return done(layerError);
      }
    
      // find next matching layer
      var layer;
      var match;
      var route;
      // 다음 매칭 레이어 찾기    
      // `match` 가 `true` 될 때까지 stack을 순회
      while (match !== true && idx < stack.length) {
        layer = stack[idx++];
        match = matchLayer(layer, path);
        route = layer.route;
    
        if (typeof match !== 'boolean') {
          layerError = layerError || match;
        }
    
        if (match !== true) {
          continue;
        }
    
        if (!route) {
          continue;
        }
    
        if (layerError) {
          match = false;
          continue;
        }
    
        var method = req.method;
        var has_method = route._handles_method(method);
    
        if (!has_method && method === 'OPTIONS') {
          appendMethods(options, route._options());
        }
    
        if (!has_method && method !== 'HEAD') {
          match = false;
        }
      }
    
      if (match !== true) {
        return done(layerError);
      }
    
      if (route) {
        req.route = route;
      }
    
      req.params = self.mergeParams
        ? mergeParams(layer.params, parentParams)
        : layer.params;
      var layerPath = layer.path;
    
      self.process_params(layer, paramcalled, req, res, function (err) {
        if (err) {
          next(layerError || err);
        } else if (route) {
          layer.handle_request(req, res, next);
        } else {
          trim_prefix(layer, layerError, layerPath, path);
        }
    
        sync = 0;
      });
    }
    
    1. matchLayer

      레이어에 path를 매칭함.

      /**
       * Match path to a layer.
       *
       * @param {Layer} layer
       * @param {string} path
       * @private
       */
      function matchLayer(layer, path) {
        try {
          return layer.match(path);
        } catch (err) {
          return err;
        }
      }
      
    2. trimprefix

      layerPathreq.url 을 비교 및 수정하고, 요청을 해당 레이어로 전달한다.

      • protohost는 요청 URL에서 프로토콜과 호스트 부분을 추출한다. 예시 : URL `http://example.com/path/to/resource` -> protohost : `http://example.com`
      • path와 layerPath 차이
        • path : 현재요청의 경로명. 예시 : 요청 URL이 /user/123 인경우 path/user/123 용도 : 요청 경로를 기준으로 레이어와 매칭 여부를 확인
        • layerPath : 미들웨어 또는 라우트가 설정된 경로. 예시 : 미들웨어가 /user 에 설정된 경우 layerPath/user. 용도 : 요청 경로의 일부분이 해당 레이어의 경로와 일치하는지 확인.
      function trim_prefix(layer, layerError, layerPath, path) {
        // 결로 길이 확인
        if (layerPath.length !== 0) {
          // Validate path is a prefix match
          // layerPath가 path의 접두사인지 확인.
          if (layerPath !== path.slice(0, layerPath.length)) {
            next(layerError)
            return
          }
      
          // Validate path breaks on a path separator
          // 뒤따르는 문자가 '/', '.' 인지 확인.
          var c = path[layerPath.length]
          if (c && c !== '/' && c !== '.') return next(layerError)
      
          // Trim off the part of the url that matches the route
          // middleware (.use stuff) needs to have the path stripped
          debug('trim prefix (%s) from url %s', layerPath, req.url);
          removed = layerPath;
          req.url = protohost + req.url.slice(protohost.length + removed.length)
      
          // protohost가 없고, req.url의 첫문자가 '/' 가 아니면 슬래시를 추가함.
          // Ensure leading slash
          if (!protohost && req.url[0] !== '/') {
            req.url = '/' + req.url;
            slashAdded = true;
          }
      
          // Setup base URL (no trailing slash)
          // parentUrl : 상위 라우터의 기본 URL
          // removed : 현재 레이어의 경로 접두사
          // 목적 `req.baseUrl` 을 상위URL과 현재 레이어의 경로로 설정하되, `removed` 의 마지막 문자가 `/` 이면 이를 제거하여 설정.
          req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
      			       ? removed.substring(0, removed.length - 1)
      			       : removed);
        }
      
        debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
        // layerError가 있으면, handle_error, 그렇지 않으면 handle_reqest
        if (layerError) {
          layer.handle_error(layerError, req, res, next);
        } else {
          layer.handle_request(req, res, next);
        }
      }
      
  2. processparams

    요청 파라미터를 처리하고, 필요한 경우 파라미터 콜백을 비동기적으로 실행한다. processparam은 param 에 등록한 콜백을 실행하는 녀석이다. param 함수는 아래처럼 생겼다.

    app.param('userId', function(req, res, next, id) {
      // 비동기 데이터베이스 조회
      User.findById(id, function(err, user) {
        if (err) {
          return next(err);
        }
        req.user = user;
        next();
      });
    });
    

    이렇게 넣으면, route parameter 에 콜백 트리거를 추가할 수 있는데, 이것을 실제로 실행하는 함수가 아래 구현이다. Route parameters 는 아래 코드를 보면 설명이 쉽다.

    Route path: /users/:userId/books/:bookId
    Request URL: http://localhost:3000/users/34/books/8989
    req.params: {"userId": "34", "bookId": "8989"}
    
    Route path: /flights/:from-:to
    Request URL: http://localhost:3000/flights/LAX-SFO
    req.params: { "from": "LAX", "to": "SFO" }
    
    Route path: /plantae/:genus.:species
    Request URL: http://localhost:3000/plantae/Prunus.persica
    req.params: { "genus": "Prunus", "species": "persica" }
    
    Route path: /user/:userId(\d+)
    Request URL: http://localhost:3000/user/42
    req.params: {"userId": "42"}
    

    이런식으로 매핑되는 값이 존재할 때, 콜백을 수행할 수 있는 듯 하다. 즉, 여기서 userId를 얻으면? user 프로퍼티에 user 정보를 가져올 수 있다는 것이다.

    proto.process_params = function process_params(layer, called, req, res, done) {
      // this는 Router객체를 말함.
    
      var params = this.params;
    
      // captured parameters from the layer, keys, and values
      var keys = layer.keys;
    
      // fast track
      // key 없으면 끝?
      if (!keys || keys.length === 0) {
        return done();
      }
    
      var i = 0;
      var name;
      var paramIndex = 0;
      var key;
      var paramVal;
      var paramCallbacks;
      var paramCalled;
    
      // process params in order
      // param callbacks can be async
      function param(err) {
        if (err) {
          return done(err);
        }
    
        if (i >= keys.length) {
          return done();
        }
    
        paramIndex = 0;
        key = keys[i++];
        name = key.name;
        paramVal = req.params[name];
        // Router.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'))) {
          //restore value
          req.params[name] = paramCalled.value;
    
          // next param
          return param(paramCalled.error);
          }
    
        called[name] = paramCalled = {
          error: null,
          match: paramVal,
          value:paramVal
          }
    
        paramCallback()
      }
    
      // single param callbacks
      function paramCallback(err) {
        var fn = paramCallbacks[paramIndex++];
    
        // store updated value
        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();
    
    }
    

1.1.4 use

1.1.5 route

1.1.6 appendMethods

1.1.7 getPathname

1.1.8 getProtohost

1.1.9

1.2 layer.js

1.2.1 Layer

Layer 모듈은 Express.js에서 라우트와 미들웨어의 경로 매칭 및 처리를 담당하는 중요한 구성 요소. 이 모듈은 경로, 옵션, 그리고 핸들러 함수를 받아 각 요청에 대해 적절한 처리를 수행.

  • path : 요청이 매칭될 경로
  • name : 함수 이름. 없으면 '<anonymous>'
  • params : 경로 매칭 옵션
  • fn : 실제 실행될 핸들러 함수
  • faststar : 모든 경로와 일치하는 정규식인지 확인.
  • fastslash : 루트 경로와 일치하는지 확인.

fast_star , fast_slack 는 최적화를 위한 것들인듯.

var pathRegexp = require('path-to-regexp');
var debug = require('debug')('express:router:layer');

module.exports = Layer;

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

1.2.2 handlererorr

단순 에러 핸들링인데, fn.length !== 4 체크로 인자의 개수를 확인하고 에러를 전파하는 듯?

여기 Layer에서 함수는 무조건 4개의 인자가 필요한가보다. 에러를 핸들링하는 함수는 err 인자가 필수이기때문에 4개의 인자가 필요하다.

그리고 예외를 던지면 그것도 next로 보낸다.

/**
 * Handle the error for the layer.
 *
 * @param {Error} error
 * @param {Request} req
 * @param {Response} res
 * @param {function} next
 * @api private
 */
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if (fn.length !== 4) {
    // not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};

1.2.3 handlerequest

여기는 일반 요청을 핸들링하는 레이어이다. 이곳은 오히려 함수의 인자가 3개를 초과하면 안된다.

/**
 * Handle the request for the layer.
 *
 * @param {Request} req
 * @param {Response} res
 * @param {function} next
 * @api private
 */

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

1.2.4 match

아마 Layer에서 가장 중요한 함수일 듯.

  1. 루트 경로 검사 (fastslash)
  2. 모든 경로 검사 (faststar)
  3. 정규 표현식 매칭 (this.regexp.exec(path))
    • 특이한점은 this.regexp 는 일반적인 정규식 매칭함수가 아니다. path-to-regexp 라는 모듈을 사용한다. [github link]​
  4. 매칭 실패 처리
  5. 매칭된 값 저장
  6. 파라미터 추출 및 디코딩
    • 정규표현식배칭을 이용한 match 로 for-loop를 돌면서 파라미터를 추출하고 디코딩한다.
/**
 * Check if this route matches `path`, if so
 * populate `.params`.
 *
 * @param {String} path
 * @return {Boolean}
 * @api private
 */

Layer.prototype.match = function match(path) {
  var match

  if (path != null) {
    // fast path non-ending match for / (any path matches)
    if (this.regexp.fast_slash) {
      this.params = {}
      this.path = ''
      return true
    }

    // fast path for * (everything matched in a param)
    if (this.regexp.fast_star) {
      this.params = {'0': decode_param(path)}
      this.path = path
      return true
    }

    // match the path
    match = this.regexp.exec(path)
  }

  if (!match) {
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  // store values
  this.params = {};
  this.path = match[0]

  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;
    var val = decode_param(match[i])

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }

  return true;
};

  1. path-to-regexp

    pathToRegexp 함수

    • 목적: 문자열 경로를 정규 표현식으로 변환. (즉, express에서 사용하는 pathsmㄴ pathToRegexp를 기반으로 만들면된다.)
    • 인자:
      • path: 문자열, 문자열 배열, 또는 정규 표현식.
      • keys: 경로에서 추출된 키를 저장할 배열.
      • options: 옵션 객체.
    • 옵션:
      • sensitive: 대소문자 구분 여부.
      • strict: 선택적 종결 구분자 허용 여부.
      • end: 문자열 끝까지 매칭 여부.
      • start: 문자열 시작부터 매칭 여부.
      • delimiter: 세그먼트의 기본 구분자.
      • endsWith: 종료 문자 리스트.
      • encode: 삽입 전 문자열을 인코딩하는 함수.
      • prefixes: 파싱 시 자동으로 접두사로 고려할 문자 리스트.
    • 주의 사항
      • 생성된 정규 표현식은 경로, 호스트 이름 등 정렬된 데이터를 대상으로 하며, 쿼리 문자열, URL 조각, JSON 등 무작위 순서의 데이터는 처리하지 못함.
      • 쿼리 문자열 포함 경로를 사용할 경우, 물음표(?)를 이스케이프 처리해야 파라미터가 선택 사항으로 간주되지 않음.
    • 사용예시 :
    const keys = [];
    const regexp = pathToRegexp("/foo/:bar", keys);
    // 결과
    // regexp = /^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i
    // keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }]
    

1.2.5 decodeparam

decodeparam 함수는 URI 인코딩된 파라미터 값을 디코딩한다. 이 함수는 Express.js의 Layer 모듈에서 사용되며, 주어진 문자열을 디코딩하여 파라미터로 반환.

/**
 * Decode param value.
 *
 * @param {string} val
 * @return {string}
 * @private
 */

function decode_param(val) {
  if (typeof val !== 'string' || val.length === 0) {
    return val;
  }

  try {
    return decodeURIComponent(val);
  } catch (err) {
    if (err instanceof URIError) {
      err.message = 'Failed to decode param \'' + val + '\'';
      err.status = err.statusCode = 400;
    }

    throw err;
  }
}

1.3 router.js

1.3.1 Route

Route 객체는 path, stack, method를 가진다.

/**
 * Initialize `Route` with the given `path`,
 *
 * @param {String} path
 * @public
 */

function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}

1.3.2 _handlesmethod

이름처럼 METHOD를 핸들링하는 듯. 작성한 method를 모두 toLowerCase()로 일반화시킨다. 주요코드는 Boolean(this.methods[name]) 이다. 해당 method 를 핸들링할 수 있는지 알려준다.

/**
 * Determine if the route handles a given method.
 * @private
 */
Route.prototype._handles_method = function _handles_method(method) {
  if (this.methods._all) {
    return true;
  }

  // normalize name
  var name = typeof method === 'string'
    ? method.toLowerCase()
    : method

  if (name === 'head' && !this.methods['head']) {
    name = 'get';
  }

  return Boolean(this.methods[name]);
};

1.3.3 options

현재 지원하는 method를 array로 리턴하는 듯? head 는 없으면 넣어준다.

/**
 * @return {Array} supported HTTP methods
 * @private
 */

Route.prototype._options = function _options() {
  var methods = Object.keys(this.methods);

  // append automatic head
  if (this.methods.get && !this.methods.head) {
    methods.push('head');
  }

  for (var i = 0; i < methods.length; i++) {
    // make upper case
    methods[i] = methods[i].toUpperCase();
  }

  return methods;
};

1.3.4 dispatch & next

req에 맞는 녀석을 dispatch 해본다.

Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  var sync = 0

  if (stack.length === 0) {
    return done();
  }
  var method = typeof req.method === 'string'
    ? req.method.toLowerCase()
    : req.method

  if (method === 'head' && !this.methods['head']) {
    method = 'get';
  }

  req.route = this;

  next();

  function next(err) {
    // signal to exit route
    if (err && err === 'route') {
      return done();
    }

    // signal to exit router
    if (err && err === 'router') {
      return done(err)
    }

    // max sync stack
    if (++sync > 100) {
      return setImmediate(next, err)
    }

    var layer = stack[idx++]

    // end of layers
    if (!layer) {
      return done(err)
    }

    if (layer.method && layer.method !== method) {
      next(err)
    } else if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }

    // Reset sync counter after each layer
    sync = 0
  }
};

코드에 대해 좀 더 자세히 뜯어봐야한다.

  1. 초기화 부분
    var idx = 0
    var stack = this.stack
    var sync = 0
    
    • idx : tracking the current position in the stack.
    • stack : References the stack of middleware and handlers associated with the route.
    • sync : Tracks the number of synchronous calls to prevent stack overflows.
  2. Empty Stack Check

    stack이 비어있으면 done() 을 호출하여 요청처리를 종료한다. (이것이 성공인지는 아직 확인필요)

  3. Normalize HTTP Method

    method를 일괄 소문자로 변경. head 가 method면 get 으로 변경.

    var method = typeof req.method === 'string'
      ? req.method.toLowerCase()
      : req.method
    
    if (method === 'head' && !this.methods['head']) {
      method = 'get';
    }
    
  4. Set Route Reference

    나중에 핸들러에서 참조할 수 있도록 현재 경로를 req.route 에 할당한다.

    req.route = this;
    

1.3.5 next function

  1. Error Handling
    • err 가 route 면 건너뛰라는 신호(현재 라우트를 종료하라)
    • err 가 router 면 전체 라우터를 종료하라는 신호
    // signal to exit route
    if (err && err === 'route') {
      return done();
    }
    
    // signal to exit router
    if (err && err === 'router') {
      return done(err)
    }
    
    
  2. Synchronous call Limitation

    call stack 초과를 방지하고자 limit을 설정하고 제한에 도달하면 setimmediate 을 사용하여 방지한다.

    // max sync stack
    if (++sync > 100) {
      return setImmediate(next, err)
    }  
    
  3. Layer Execution

    stack 에서 다음 layer를 가져온다. 더 이상 layer가 없으면 건너뛴다.

    var layer = stack[idx++]
    
    // end of layers
    if (!layer) {
      return done(err)
    }
    
  4. Matching

    현재 메서드와 일치하지 않으면 next 호출하여 현재 레이어를 건너뛴다.

    오류가 있으면, layer.handler_error 를 호출한다.

    오류가 없으면, layer.handle_request 를 호출한다.

    if (layer.method && layer.method !== method) {
      next(err)
    } else if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
    

1.3.6 all function

주석의 설명처럼. .all 을 호출하면 모든 verbs 에서 핸들링하는 듯.

.all 함수의 인자가 함수가 아니면 예외를 던진다.

/**
 * Add a handler for all HTTP verbs to this route.
 *
 * Behaves just like middleware and can respond or call `next`
 * to continue processing.
 *
 * You can use multiple `.all` call to add multiple handlers.
 *
 *   function check_something(req, res, next){
 *     next();
 *   };
 *
 *   function validate_user(req, res, next){
 *     next();
 *   };
 *
 *   route
 *   .all(validate_user)
 *   .all(check_something)
 *   .get(function(req, res, next){
 *     res.send('hello world');
 *   });
 *
 * @param {function} handler
 * @return {Route} for chaining
 * @api public
 */

Route.prototype.all = function all() {
  var handles = flatten(slice.call(arguments));

  for (var i = 0; i < handles.length; i++) {
    var handle = handles[i];

    if (typeof handle !== 'function') {
      var type = toString.call(handle);
      var msg = 'Route.all() requires a callback function but got a ' + type
      throw new TypeError(msg);
    }

    var layer = Layer('/', {}, handle);
    layer.method = undefined;

    this.methods._all = true;
    this.stack.push(layer);
  }

  return this;
};

1.3.7 methods.forEach

get, post, put, delete 등의 메서드를 Route 객체에 추가한다.

  • Route.prototype[method] 를 보면 각 메서드이름을 프로퍼티로 함수를 주입니다.
  • 인자가 함수가 아니면 가차없이 Error를 던진다.
  • 루프 Layer를 만들고, stack에 push한다.
  • Layer 객체는 각 미들웨어 또는 핸들러를 캡슐화하여 특정 경로와 메서드에 연결한다. 요청이 들어올 때 해당 경로와 메서드에 맞는 핸들러를 실행합니다.
  • stack 배열은 라우트에 연결된 모든 미들웨어와 핸들러를 순차적으로 저장한다. 요청이 처리될 때 이 배열을 순회하며 각 핸들러를 실행한다.
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
	var type = toString.call(handle);
	var msg = 'Route.' + method + '() requires a callback function but got a ' + type
	throw new Error(msg);
      }

      debug('%s %o', method, this.path)

      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});
  1. method library source code

    methods 라이브러리는 단순한 한 페이지 소스코드를 갖고 있다.

    최슨 Node.js 에서 지원하는 method들과 이전 버전을 위한 상수 배열을 갖고 있다.

         var http = require('http')
    
         module.exports = getCurrentNodeMethods() || getBasicNodeMethods()
    
       /**
        * Get the current Node.js methods.
        * @private
        */
       function getCurrentNodeMethods () {
         return http.METHODS && http.METHODS.map(function lowerCaseMethod (method) {
           return method.toLowerCase()
         })
       }
    
      /**
      * Get the "basic" Node.js methods, a snapshot from Node.js 0.10.
      * @private
      */
    function getBasicNodeMethods () {
      return [
        'get',
        'post',
        'put',
        'head',
        'delete',
        'options',
        'trace',
        'copy',
        'lock',
        'mkcol',
        'move',
        'purge',
        'propfind',
        'proppatch',
        'unlock',
        'report',
        'mkactivity',
        'checkout',
        'merge',
        'm-search',
        'notify',
        'subscribe',
        'unsubscribe',
        'patch',
        'search',
        'connect'
      ]
    }
    
    

    [github link]​

Author: Younghwan Nam

Created: 2024-12-20 Fri 16:33

Emacs 27.2 (Org mode 9.4.4)

Validate