简写 Express

356 阅读6分钟

最近需要用到 Express,将 Express 学习了一下。Express 提供了丰富的功能,但是目前,我基本只会用到 Express 的路由和模板功能,所以我只是将 Express 的路由机制了解了一下。Express 的思想并不复杂,但是由于 Express 支持的功能多了,代码也就复杂了。我将 Express 的路由机制简写了下,方便以后自己进一步学习,同时,其他同学也多了份参考。

一、支持 app.use(path, fn)

测试代码如下:

// test/test-use.js

const express = require('../lib/express');

const app = express();

app.set('case sensitive routing', true);

app.use(fn1);

app.use('/a', fn2);

app.listen(3000);

function fn1(req, res, next) {
  console.log('fn1 exec \n');
  next();
}

function fn2(req, res, next) {
  console.log('fn2 exec \n');

  console.log('app: ', app, '\n')

  console.log('app._router: ', app._router, '\n');

  next();
}

对于上面的测试代码,我们预期的结果是:对于 fn1,任何请求进来,fn1 都会执行,对于 fn2,只有请求路径包含 /a(包含指请求路径为 /a、/a/、/a/b 等等,注意:/ab 并不包含 /a ),fn2 才会执行。我们能想到,可以声明一个空数组,每当执行一次 app.use,就把传入 use 的函数,加入到数组中,当请求到来时,依次执行数组中的函数即可,这样实现,则 app.use(fn) 是符合预期的。但是 app.use(path, fn) 是不符合预期的,我们稍微改造下前面的实现,就能让 app.use(path, fn) 也符合预期,我们还是先申明一个数组,每当执行一次 app.use 时,就把传入 app.use 的路径和函数,挂在一个 Layer 对象上,然后把这个对象放入数组,当请求到来时,我们遍历数组中的对象,如果请求路径与 Layer 对象上挂的路径匹配,我们则执行挂在 Layer 上的方法。这样实现 app.use(path, fn) 就是符合预期的了,下面代码就是这个思路的实现:

代码目录如下:

lib/express.js 文件:

// lib/express.js

const proto = require('./application');

exports = module.exports = createApplication;

function createApplication() {
  // 执行 express(),返回此函数,同时,此函数也是请求到来时的回调函数
  const app = function(req, res) {
    app.handle(req, res);
  }

  Object.assign(app, proto);

  app.init();
  return app;
}

lib/application.js 文件:

// lib/application.js

/**
 * 此文件用来给 app 添加各种属性和函数
 */

const Router = require('./router');
const http = require('http');
const slice = Array.prototype.slice;

const app = exports = module.exports = {};

app.init = function init() {
  this.settings = {};
};

app.listen = function listen() {
  const server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

app.use = function use(fn) {
  let offset = 0;
  let path = '/';

  // 兼容 app.use(path, fn)
  if(typeof fn !== 'function') {
    offset = 1;
    path = fn;
    
  }

  const fns = slice.call(arguments, offset);

  this.lazyrouter();
  const router = this._router;

  fns.forEach(function(fn) {
    router.use(path, fn);
  });

  return this;
};

app.lazyrouter = function lazyrouter() {
  if(!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
    });
  }
};

app.enabled = function enabled(setting) {
  return Boolean(this.set(setting));
};

app.set = function set(setting, val) {
  if(arguments.length === 1) {
    return this.settings[setting];
  }

  this.settings[setting] = val;

  return this;
}

app.handle = function handle(req, res) {
  const router = this._router;

  // 兜底函数,当所有中间件遍历完时,还没有执行 res.end,则由这个函数兜底,返回响应
  const done = function() {
    res.end(`Cannot ${req.method} ${req.url}`);
  };

  router.handle(req, res, done);
}

lib/router/index.js 文件:

// lib/router/index.js

const url = require('url')
const Layer = require('./layer');

const slice = Array.prototype.slice;

const proto = module.exports = function(options) {
  const opts = options || {};

  function router() {

  }

  Object.setPrototypeOf(router, proto);

  router.caseSensitive = opts.caseSensitive;
  router.stack = [];

  return router;
};

proto.use = function use(fn) {
  let offset = 0;
  let path = '/';

  if(typeof fn !== 'function') {
    offset = 1;
    path = fn;
  }

  const callbacks = slice.call(arguments, offset);
  
  for(let i = 0; i < callbacks.length; i++) {
    const fn = callbacks[i];

    const layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
};

proto.handle = function(req, res, out) {
  const self = this;

  let idx = 0;

  const stack = self.stack;

  next();

  function next(err) {
    if(idx >= stack.length) {
      out(err);
      return;
    }

    const { pathname } = url.parse(req.url, true);

    let layer;
    let match;
    let route;

    while(match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = layer.match(pathname);
      route = layer.route;

      if(match !== true) {
        continue;
      }

      if(!route) {
        continue
      }
    }

    if(match !== true) {
      return out(err);
    }

    trim_prefix(layer, err);
  }

  function trim_prefix(layer, err) {
    if(err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

lib/layer.js 文件:

// lib/layer.js

const pathRegexp = require('path-to-regexp');

module.exports = Layer;

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

  const opts = options || {};

  this.handle = fn;
  this.regexp = pathRegexp(path, opts);

  this.regexp.fast_slash = path === '/' && opts.end === false
}

Layer.prototype.match = function match(path) {
  let match;

  if(this.regexp.fast_slash) {
    return true;
  }

  match = this.regexp.exec(path);

  if(!match) {
    return false;
  }

  return true;
};

Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  const fn = this.handle;

  if(fn.length !== 4) {
    return next(error);
  }

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

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

  if(fn.length > 3) {
    return next();
  }

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

node_modules/path-to-regexp/index.js 文件:

// node_modules/path-to-regexp/index.js

module.exports = pathtoRegexp;

function pathtoRegexp(path, options) {
  options = options || {};
  var strict = options.strict;
  var end = options.end !== false;
  var flags = options.sensitive ? '' : 'i';

  path = ('^' + path + (strict ? '' : path[path.length - 1] === '/' ? '?' : '/?'))
    .replace(/(\/)/g, '\\$1');

  path += (end ? '$' : (path[path.length - 1] === '/' ? '' : '(?=\\/|$)'));

  return new RegExp(path, flags);
};

当执行完 test/test-use.js 之后,会生成如下的数据结构:

从上图可以看出,app 有一个 _router 属性,_router 属性有一个 stack 属性,stack 是一个数组,我们调用 app.use 时,会生成 Layer 实例,并将 layer 放入到 stack 中,其中 layer 上有个 handle 和 regexp 属性,handle 指向的就是传入 app.use 的函数,regexp 依据传入 app.use 的路径生成。

当请求到来时,会依次遍历 stack 中的 layer,如果请求路径与 layer 上的路径匹配,则执行 layer.handle,如果不匹配,则遍历下一个 layer。

我们可以发现,app.use 的思想还是比较简单,就是常规思路。

二、支持 app.get(path, fn1, fn2)

测试代码如下:

// test/test-get.js

const express = require('../lib/express');

const app = express();

app.set('case sensitive routing', true);

app.set('strict routing', true);

app.use(fn1);

app.get('/a', fn2, fn3);

app.listen(3000);

function fn1(req, res, next) {
  console.log('fn1 exec \n');
  next();
}

function fn2(req, res, next) {
  console.log('fn2 exec \n');
  next();
}

function fn3(req, res, next) {
  console.log('fn3 exec \n');

  console.log('app: ', app, '\n')

  console.log('app._router: ', app._router, '\n');

  console.log('app._router.stack[1]: ', app._router.stack[1], '\n');

  next();
}

对于上面的测试代码,我们预期的结果是,对于任何请求都执行 fn1,当请求路径全等于 /a,且请求方法为 get 时,执行 fn2 和 fn3,前面的代码对于中间件是否执行,只比较了路径,不能支持 app.get。我们可以做如下改动,使得,改动后的代码支持 app.get。首先,执行 app.use,生成一个 Layer 对象,将传入 app.use 的路径和方法挂在 layer 对象上,并将 layer 对象放入 app._router.stack 中;执行 app.get,生成一个 Layer 对象,再生成一个 Route 对象,将请求方法和传入 app.get 的函数挂在 route 对象上,将 route 以及遍历挂在 route 的函数的方法,挂在 layer 对象上。

当请求到来时,遍历 app._router.stack,如果 layer 对象上的 route 属性不为真,则只要路径匹配,就执行挂在 layer 上的方法,如果 layer 对象上的 route 属性为真,首先看请求路径是否与挂在 layer 上的路径匹配,如果匹配,在看下请求方法是否和挂在 route 上的方法匹配,如果匹配,则执行挂在 layer 上的遍历 route.stack 的函数。

改动后的代码目录如下:

代码变动:

lib/application.js 文件变动:

// lib/application.js

// add
const methods = require('methods');

app.lazyrouter = function lazyrouter() {
  if(!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      // add
      strict: this.enabled('strict routing')
    });
  }
};

// add
methods.forEach(function(method) {
  app[method] = function(path) {
    if(method === ' get' && arguments.length === 1) {
      return this.set(path);
    }

    this.lazyrouter();

    const route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  }
});

lib/router/index.js 文件变动:

// lib/router/index.js

// add
const Route = require('./route');

const slice = Array.prototype.slice;

const proto = module.exports = function(options) {
  const opts = options || {};

  function router() {

  }

  Object.setPrototypeOf(router, proto);

  router.caseSensitive = opts.caseSensitive;
  // add
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

proto.handle = function(req, res, out) {
  const self = this;

  let idx = 0;

  const stack = self.stack;

  next();

  function next(err) {
    if(idx >= stack.length) {
      out(err);
      return;
    }

    const { pathname } = url.parse(req.url, true);

    let layer;
    let match;
    let route;

    while(match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = layer.match(pathname);
      route = layer.route;

      if(match !== true) {
        continue;
      }

      if(!route) {
        continue
      }

      // add
      const method = req.method;
      const has_method = route._handles_method(method);

      if(!has_method) {
        match = false;
        continue;
      }
    }

    if(match !== true) {
      return out(err);
    }

    trim_prefix(layer, err);
  }

  function trim_prefix(layer, err) {
    if(err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

// add
proto.route = function route(path) {
  const route = new Route(path);

  const layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
}

新增 lib/router/route.js 文件

// lib/router/route.js

const Layer = require('./layer');
const methods = require('methods');

const slice = Array.prototype.slice;

module.exports = Route;

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

  this.methods = {};
}

methods.forEach(function(method) {
  Route.prototype[method] = function() {
    const handles = slice.call(arguments);

    for(let i = 0; i < handles.length; i++) {
      const layer = Layer('/', {}, handles[i]);
      layer.method = method;

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

    return this;
  };
});

Route.prototype._handles_method = function _handles_method(method) {
  const name = method.toLowerCase();

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

Route.prototype.dispatch = function dispatch(req, res, done) {
  let idx = 0;
  const stack = this.stack;
  if(stack.length === 0) {
    return done;
  }

  next();

  function next(err) {
    const layer = stack[idx++];

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

新增 node_modules/methods/index.js 文件

// node_modules/methods/index.js

module.exports = ['get'];

执行 /test/test-get.js 文件,我们可以得到如下的数据结构:

我们可以看到通过 app.get 生成的 Layer 对象,layer.route 是一个 Route 对象,route 对象上记录了方法,用于请求到来时,方法比较,route.stack 中有 2 个 Layer 对象,这两个 layer 对象上的 handle 属性分别是 fn2,fn3,其实我感觉,route.stack 中直接存 fn2、fn3 也是可以的。

可以看出,app.get 只是在 app.use 的数据结构上,加了点变动,有点像二位数组的遍历,思想也是较为简单。

三、支持 app.use(path, router)

了解了 app.use(path, fn) 及 app.get(path, fn1, fn2) 的实现思路后,看下代码就知道 app.use(path, router) 实现的功能及实现思路,下面是支持 app.use(path, router) 的代码改动,改动很小。

代码变动:

lib/router/index.js 文件变动:

// lib/router/index.js

const proto = module.exports = function(options) {
  const opts = options || {};

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

  Object.setPrototypeOf(router, proto);

  router.caseSensitive = opts.caseSensitive;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

proto.handle = function(req, res, out) {
  const self = this;

  let idx = 0;

  const stack = self.stack;

  // add
  let slashAdded = false;
  let removed = '';

  next();

  function next(err) {
    // add
    if (slashAdded) {
      req.url = '';
      slashAdded = false;
    }
    if (removed.length > 0) {
        req.url = removed + req.url;
        removed = '';
    }

    if(idx >= stack.length) {
      out(err);
      return;
    }

    const { pathname } = url.parse(req.url, true);

    let layer;
    let match;
    let route;

    while(match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = layer.match(pathname);
      route = layer.route;

      if(match !== true) {
        continue;
      }

      if(!route) {
        continue
      }

      const method = req.method;
      const has_method = route._handles_method(method);

      if(!has_method) {
        match = false;
        continue;
      }
    }

    if(match !== true) {
      return out(err);
    }

    trim_prefix(layer, err);
  }

  function trim_prefix(layer, err) {
    // add
    removed = layer.path;
    req.url = req.url.slice(removed.length);
    if (req.url == '') {
      req.url = '/';
      slashAdded = true;
    }
    
    if(err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

// add
methods.forEach(function(method) {
  proto[method] = function(path) {
    const route = this.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return;
  }
})

lib/layer.js 文件变动:

// lib/layer.js

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

  const opts = options || {};

  // add
  this.path = path;

  this.handle = fn;
  this.regexp = pathRegexp(path, opts);

  this.regexp.fast_slash = path === '/' && opts.end === false
}

以上,就是我简写的 Express 路由机制,路由机制中的参数处理,我没有写,因为我似乎用不到。可以看出,Express 思路是较为简单。感觉 Express 的目标实现比较全的功能,比如模板、缓存,但是如果用了服务端渲染、将静态文件托管 CDN,其实这两个功能用的就比较少了,但是 Express 用于学习服务端还是不错的。

四、总结

略。。。

更新日志:

2021-02-18:发布

参考资料