手撕 express

115 阅读19分钟

express 和 koa 是同班人马打造,属于上一代的产物,然而如今还是有着大量使用,它和 koa 是有一些不同的哦。

express 对比 koa

  1. express 处理请求的时候全部采用的回调函数的方式,而 koa 采用的是 async + await
  2. express 内部采用的是 ES5 语法,koa 采用的是 es6 来编写的
  3. express 比 koa 丰富,也更重,多了一些内置的中间件(路由,静态服务,模板渲染等)
  4. koa 中为了扩展采用的方式是在 ctx 扩展了 request,response 对象,而 express 直接在原生的 req 和 res 的基础上进行了扩展
  5. express 中的特点是内部采用了回调的方式来组合代码,koa 支持转 promsie 串联

express 基础用法

const express = require('express');

// exprress 创建应用是通过 express() 来创建的
const app = express();

// 可以看出来 express 集成了路由功能
app.get('/', function(req, res) {
  res.end('home');
});

app.get('/hello', function(req, res) {
  res.end('hello');
});

app.all('*', function(req, res) {
  res.end('all');
});

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

实现基础版 express

- myExpress
	- lib
		- express.js
	- index.js 

index.js,入口文件,暴露 express 模块。

module.exports = require('./lib/express');

lib/express.js

  1. 根据用户调用的 app.get 方法(all 暂未处理)收集路由表
  2. 根据 listen 传递的参数,创建服务,路由查询,cb 调用。

myExpress/index.js,入口文件,暴露 express 模块。

lib/express.js
const http = require('http');
const url = require('url');
let routers = [{
  method: 'all',
  path: '*',
  handler(req, res) {
    res.end(`Cannot ${ req.method } ${ req.url }`);
  }
}];

function createApplication() {
  return {
    get(path, handler) {
      routers.push({
        method: 'get',
        path,
        handler
      });
    },
    listen(...args) {
      // listen 创建服务
      const server = http.createServer(function(req, res) {
        // 最后一个参数 true 会把我们的 query 转成一个对象
        let { pathname, query } = url.parse(req.url, true);
        let requestMethod = req.method.toLowerCase();

        // 在路由表中匹配 i 从 1 开始,过滤到顶部默认的 all 方法
        for (let i = 1; i < routers.length; i++) {
          let { method, path, handler } = routers[i];

          // 匹配请求路径和请求方法,执行对应回调
          console.log(pathname, method, routers);
          if (pathname == path && method == requestMethod) {
            return handler(req, res);
          }
        }

        // 如果没有匹配到,则返回兜底 all 的处理方式,默认是抛错
        return routers[0].handler(req, res);
      });

      server.listen(...args);
    },
    all() {
      // 暂未处理 app.all 方法
    }
  }
}

module.exports = createApplication;

这样,就完成了基础的带有 get 路由的 express

演化一:工厂模式创建 express 应用

我们发现,createApplication 函数很冗余,我们把创建应用这一步提成一个工厂类。

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

function createApplication() {
  return new Application();
}

module.exports = createApplication;

lib/application.js
const http = require('http');
const url = require('url');

function Application() {
  this._routers = [{
    method: 'all',
    path: '*',
    handler(req, res) {
      res.end(`Cannot ${ req.method } ${ req.url }`)
    }
  }];
}

Application.prototype.get = function (path, handler) {
  this._routers.push({
    path,
    method: 'get',
    handler
  })
}

Application.prototype.listen = function (...args) {
  // listen 创建服务
  const server = http.createServer((req, res) => {
    // 最后一个参数 true 会把我们的 query 转成一个对象
    let { pathname, query } = url.parse(req.url, true);
    let requestMethod = req.method.toLowerCase();

    // 在路由表中匹配 i 从 1 开始,过滤到顶部默认的 all 方法
    console.log(this._routers);
    for (let i = 1; i < this._routers.length; i++) {
      let { method, path, handler } = this._routers[i];

      // 匹配请求路径和请求方法,执行对应回调
      console.log(pathname, method, this._routers);
      if (pathname == path && method == requestMethod) {
        return handler(req, res);
      }
    }

    // 如果没有匹配到,则返回兜底 all 的处理方式,默认是抛错
    return this._routers[0].handler(req, res);
  });

  server.listen(...args);
}

module.exports = Application

演化二: 应用 & 路由分离

我们发现,虽然把应用代码抽离成一个类,但是路由的代码和应用的代码耦合在一起,这样也是不好的,我们继续拆。

- myExpress
	- lib
		- router 
			- index.js  // 路由系统
		- application.js
		- express.js
	- index.js 
lib/application.js
const http = require('http');
const url = require('url');
const Router = require('./router');

// 每个应用默认创建一个路由系统
function Application() {
  this.router = new Router();
}

// app.get,调用路由系统的 get 方法收集路由
Application.prototype.get = function (path, handler) {
  this.router.get(path, handler);
}

// app.listen, 服务启动,路由系统暴露一个方法 handle,处理当前路由
Application.prototype.listen = function (...args) {
  const server = http.createServer((req, res) => {
    function done() {
      res.end(`Cannot ${req.method} ${req.url}`)
    }

    // 交给路由系统处理 路由系统处理不了调用 done 方法
    this.router.handle(req, res, done);
  });
  server.listen(...args);
}

module.exports = Application

lib/router/index.js
const url = require('url');
function Router() {
  this.stack = [];
}

// 向路由的 stack 添加
Router.prototype.get = function (path, handler) {
  this.stack.push({
    path,
    method: 'get',
    handler
  })
}

// 请求到来时,会匹配对应的路由
Router.prototype.handle = function (req, res, done) {
  let { pathname, query } = url.parse(req.url, true);
  let requestMethod = req.method.toLowerCase();

  for (let i = 0; i < this.stack.length; i++) {
    let { path, method, handler } = this.stack[i];

    if (pathname === path && method === requestMethod) {
      return handler(req, res);
    }
  }

  done()
}

module.exports = Router

兼容路由中间件和同路径不同方法的路由串联写法

实际上我们开发的过程中,匹配到路由之后,执行回调之前,我们通常会调用一些中间件,比如鉴权中间件,使用方式如下

// 路由的中间件功能
const express = require('express');
const app = express();

function checkAuth(req, res, next) {
  if (req.query.auth == '1') {
    next();
  } else {
    res.end('no auth');
  }
}

function logger(req, res, next) {
  console.log('logger run');
  next();
}

// 写法 1
app.get('/', logger, checkAuth, function(req, res, next) {
  res.end('ok')
})

// 写法 2
app.get('/', [logger, checkAuth], function(req, res, next) {
    res.end('ok')
})

app.listen(3000);

中间件的执行顺序

const express = require('express');
const app = express();

app.get('/', function(req, res, next) {
  console.log(1);
  next();
  console.log(2)
}, function(req, res, next) {
  console.log(3);
  next();
  console.log(4)
}, function(req, res, next) {
  console.log(5);
  next();
  console.log(6)
})

app.get('/', function(req, res, next) {
  console.log(7);
  next();
})

app.listen(3000);

// 1 3 5 7 6 4 2

执行顺序跟 koa 是没区别的,但是 next 调用没有等待效果。

更恶心的是,express 提供了下面这种不推荐的写法,不过也可以执行(暂未实现)。

// app.route('/').post(function(req,res){
//     res.end('post')
// }).get(function(req,res){
//     res.end('get')
// })

ok,以上就是 express 中路由中间件和串联路由声明的写法,我们怎么设计路由,能兼容这么多种调用方式呢。

如下图所示:

1. 用户注册路由(比如 app.get)创建一个 layer 实例和一个 route 实例,并且 layer.route = route,layer 上保存有当前注册的路径和一个回调(route.dispatch,该回调会检查自身 stack,找到符合条件的 cb 依次执行),然后把当前 layer 放到 router 的栈内用作路径匹配。 2. 根据当前路由注册的请求方法,创建一个个 layer 实例,其上保存了当前的请求方式和回调函数(这个回调函数是用户本身的回调函数),然后存到 route 的栈中用作当请求触发,进行方法匹配,依次执行回调。 3. 优化一:route 中保存着针对当前路径,收集到的路由包含哪些注册的方法(get,post 等),如果没有当前请求对应的方法,直接下跳。 4. 优化二:express() 时不再直接创建路由,进行路由的懒初始化,可能有些人只创建应用而不想初始化路由。

实际请求到来时,先根据 path 匹配 router 中存储的 layer,然后调用 layer.hanlder(其实也就是它对应的 ruote.dispatch) 去依次遍历 route 中存储的已注册的路由回调,找到对应的请求方法的回调依次执行。

- myExpress
	- lib
		- router 
			- index.js  // 路由系统
			- layer.js  // 存储路径 -> route.diapatch 方法的映射
			- route.js  // 存储请求 method、回调任务,并提供 diapatch 派发执行
		- application.js
		- express.js
	- index.js 

这个逻辑比较绕一点,好好理解一下哦,最好跟 手撕 koa2 对比学习。

lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');

function Router() {
  this.stack = [];
}
Router.prototype.route = function (path) {
  let route = new Route();
  let layer = new Layer(path, route.dispatch.bind(route));
  layer.route = route;

  this.stack.push(layer);

  return route;
}

methods.forEach(method => {
  Router.prototype[method] = function (path, handlers) { // 像路由的stack中添加
    let route = this.route(path); // 创建route,并返回route
    route[method](handlers)
  }
})

Router.prototype.handle = function (req, res, done) {
  // 要在路由的栈中查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
  const { pathname } = url.parse(req.url);
  const method = req.method.toLowerCase();
  let i = 0
  const next = () => {
    if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
    let layer = this.stack[i++];

    if (layer.match(pathname)) {
      if (layer.route.methods[method]) {
        // 这个 next 传进去 route.dispatch 内部,当内部执行完,调用该 next
        // 为了形象,在 route.dispatch 中我们把该 next 命名为 out
        layer.handle_request(req, res, next); // route.dispatch
      } else {
        next();
      }
    } else {
      next();
    }
  }
  next();
}
module.exports = Router

lib/router/layer.js
// 存一个路径和 handler 方法的对象
function Layer(path,handler){
  this.path = path;
  this.handler = handler;
}

Layer.prototype.match = function(pathname){
  return this.path == pathname;
}

// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
  return this.handler(req,res,next)
}

module.exports = Layer;


lib/router/route.js
const Layer = require('./layer');
const methods = require('methods')
function Route() {
  this.stack = [];
   // 用来标识route上包含哪些方法
   // 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
  this.methods = {};
}

// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function (req, res, out) {
  let i = 0;
  let next = () => {
    if (i === this.stack.length) return out();
    let layer = this.stack[i++];

    if (layer.method === req.method.toLowerCase()) {
      layer.handler(req, res, next); // 用户注册的回调
    } else {
      next();
    }
  }
  next();
}

methods.forEach(method => {
  Route.prototype[method] = function (handlers) {
    handlers.forEach(handler => {
      // 这里路径其实没有用到,不一定是 /,随便传哦。
      let layer = new Layer('/', handler);
      layer.method = method;// 给每一层都添加一个方法
      this.methods[method] = true;
      this.stack.push(layer);
    })
  }
})

module.exports = Route;

lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
  // this.router = new Router();
}

// 懒加载路由
Application.prototype.lazy_route = function () {
  if (!this.router) {
    this.router = new Router();
  }
}

methods.forEach(method => {
  // app.get,app.post 等
  Application.prototype[method] = function (path, ...handlers) { 
    // 懒加载路由
    this.lazy_route();
    this.router[method](path, handlers); // 像路由系统中添加
  }
})

// app.listen()
Application.prototype.listen = function (...args) { 
  const server = http.createServer((req, res) => {
    function done() {
      res.end(`Cannot ${req.method} ${req.url}`)
    }
    this.lazy_route();
    this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
  });

  server.listen(...args)
}
module.exports = Application

express 中间件 + 错误处理

如果我们路由中间件比较多,我们没必要 app.method(path, [fn1, fn2])依次把方法填到路由中间件上,那能不能把中间件单独抽离出来。

const express = require('express');
const app = express();

// 针对所有路由的中间件,可以决定是否向下执行,可以扩展方法和属性
app.use('/', function(req, res, next) {
  console.log('all middleware1');
  next();
}, function(req, res, next) {
  console.log('all middleware2');
  next();
});

// 针对 /user 路由开头的中间件
app.use('/user', function(req, res, next) {
  console.log('user middleware');
  // next 方法传递参数代表错误
  next('error');
});

app.get('/user', function(req, res) {
  res.end('user');
});

// 前缀匹配到也能执行哦
app.get('/user/info', function(req, res) {
  res.end('user info');
});

// 错误处理中间件 一般放在页面最底部去做兜底错误处理
// 当 next 方法传递参数,代表发生错误
app.use((error, req, res, next) => {

});

app.listen(3000, function() {
  console.log(`server start 3000`);
});

可以看到,use 方法用来注册中间件,而且 use 可能接收多个处理方法,且中间件执行是在路由匹配之前。

具体思路如下图所示:

  1. 中间件类似路由,每次创建一个新的 layer 存入 router 中,不传路径则默认 "/",如果一个中间件内部有多个方法,则按回调方法拆分,重新注册成一个个的 layer 塞进 router。
  2. 中间件不需要 route 实例,中间件内部没有请求方式,只存了路径 + 一个回调函数,只要匹配到当前路由的 layer 且 layer 上没有 route(说明是中间件),直接调用 handler 方法执行,这里 handler 方法就是用户传进来的中间件回调。
  3. 错误处理中间件写在程序底部,用于错误兜底,中间件中 next('some things') 代表错误传递,会被兜底的错误捕获,且中间件错误传递会跳过所有路由,直达错误处理中间件,注意路由的中间件也能通过 next 传递错误直达错误中间件(它和中间件的 next 不一样哦,一个是竖着传递,一个是横着传递)。
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');

function Router() {
  this.stack = [];
}
Router.prototype.route = function (path) {
  let route = new Route();
  let layer = new Layer(path, route.dispatch.bind(route));
  layer.route = route;

  this.stack.push(layer);

  return route;
}

Router.prototype.use = function (path) {
  let args = Array.from(arguments);
  let handlers = [];
  // 如果第一个参数不传,默认是 '/'
  if (typeof path === 'function') {
    path = '/';
    handlers = [...args]
  } else {
    handlers = args.slice(1)
  }

  handlers.forEach(handler => {
    let layer = new Layer(path, handler);
    layer.route = undefined; // 后面判断 如果layer上有route属性 说明是路由,没有说明是中间件
    this.stack.push(layer);
  })
}

methods.forEach(method => {
  Router.prototype[method] = function (path, handlers) { // 像路由的stack中添加
    let route = this.route(path); // 创建route,并返回route
    route[method](handlers)
  }
})

Router.prototype.handle = function (req, res, done) {
  // 要在路由的栈种查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
  const { pathname } = url.parse(req.url);
  const method = req.method.toLowerCase();
  let i = 0
  const next = (err) => {
    if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
    let layer = this.stack[i++];
    if (err) {
      // 如果有错误就在栈中查找错误处理中间件,不是错误处理中间件的就不要执行了
      if (!layer.route) {
        // 四个参数代表是错误中间件
        if (layer.handler.length === 4) {
          layer.handler(err, req, res, next);
        } else {
          next(err); // 正常的中间件
        }
      } else {
        next(err); // 路由
      }
    } else {
      // 无论路由还是中间件 都得匹配路径,但是中间件不匹配方法
      if (layer.match(pathname)) { // layer种用来管理自己的匹配逻辑
        if (!layer.route) { // 中间件不需要匹配方法
          if (layer.handler.length === 4) { // 如果正常情况下是不执行错误处理中间件的
            next();
          } else {
            layer.handle_request(req, res, next)
          }
        } else { // 路由需要匹配方法,在执行
          if (layer.route.methods[method]) {
            layer.handle_request(req, res, next); // route.dispatch
          } else {
            next();
          }
        }
      } else {
        next();
      }
    }
  }
  next();
}
module.exports = Router

lib/router/layer.js
// 存一个路径和 handler 方法的对象
function Layer(path,handler){
  this.path = path;
  this.handler = handler;
}

Layer.prototype.match = function(pathname) {
  // 无论中间件layer 还是路由layer  只要一样肯定匹配到
  if(this.path == pathname){
       return true;
  }
  // 如果是中间件 我们开头匹配就可以
  if(!this.route){
      if(this.path == '/'){ // 中间件路径是/ 表示可以匹配到
          return true;
      }
      // /user/info  /user/
      // 为了避免 /u 也被匹配,我们加 '/' 匹配
      return pathname.startsWith(this.path + '/');
  }
  return false;
}

// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
  return this.handler(req,res,next)
}

module.exports = Layer;


lib/router/route.js
const Layer = require('./layer');
const methods = require('methods')
function Route() {
  this.stack = [];
   // 用来标识route上包含哪些方法
   // 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
  this.methods = {};
}

// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function(req,res,out){
  let i = 0;
  let next = (err) =>{ // 内部路由抛出错误 我就将错误派发到外层处理
      if(err) return out(err);
      if(i === this.stack.length) return out();
      let layer = this.stack[i++];
      if(layer.method === req.method.toLowerCase()){
          layer.handler(req,res,next); // 用户注册的回调
      }else{
          next();
      }
  }
  next();
}

methods.forEach(method => {
  Route.prototype[method] = function (handlers) {
    handlers.forEach(handler => {
      // 这里路径其实没有用到,不一定是 /,随便传哦。
      let layer = new Layer('/', handler);
      layer.method = method;// 给每一层都添加一个方法
      this.methods[method] = true;
      this.stack.push(layer);
    })
  }
})

module.exports = Route;

lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
  // this.router = new Router();
}

// 懒加载路由
Application.prototype.lazy_route = function () {
  if (!this.router) {
    this.router = new Router();
  }
}

methods.forEach(method => {
  // app.get,app.post 等
  Application.prototype[method] = function (path, ...handlers) { 
    // 懒加载路由
    this.lazy_route();
    this.router[method](path, handlers); // 像路由系统中添加
  }
})

// app.use()
Application.prototype.use = function(path, ...handler) {
  this.lazy_route();
  this.router.use(...arguments);
}

// app.listen()
Application.prototype.listen = function (...args) { 
  const server = http.createServer((req, res) => {
    function done() {
      res.end(`Cannot ${req.method} ${req.url}`)
    }
    this.lazy_route();
    this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
  });

  server.listen(...args)
}
module.exports = Application

正则路由

我们注册路由时候的路径并不一定是单纯的字符串,我们可能想写正则,想去接收参数,比如

// 我的路径必须是 /user/随意的id/随意的名字/xxx
app.get('/user/:id/:name/xxx', function (req, res) {
  // params: { id: 随意的id, name: 随意的名字 }
  res.end(JSON.stringify(req.params)) 
})

访问 http://localhost:3000/user/1/ys/xxx,我期待页面输出 {"id":"1","name":"ys"},这个问题我们拆成两步。

  1. 当注册路由时,根据路由 path 生成正则,这里用了一个包,叫 "path-to-regexp"
  2. 请求到来时,通过正则去匹配路由,并把匹配到的动态参数解析为 params,挂载到 layer 上,然后路由处理时,req.params = layer.params
lib/router/layer.js
const pathToRegExp = require('path-to-regexp');

function Layer(path,handler){
  this.path = path;
  this.keys = [];
  // 将请求路径转成用于匹配的正则,并记录出现的 keys
  this.regexp = pathToRegExp(this.path, this.keys); 
  // keys: [{ name: 'id' }, { name: 'name' } ]
  // regexp: /^\/user\/(?:([^\/]+?))\/(?:([^\/]+?))\/xxx\/?$/i 用于匹配真实 path
  this.handler = handler; 
}

Layer.prototype.match = function(pathname) {
  const matches = pathname.match(this.regexp); // [1] id  [2] name

  // 如果 matches 存在,说明是正则路由,在 layer 上挂载解析好的 params 属性
  if (matches){
    // { id: '1', name: ':ys' }
    this.params = this.keys.reduce((memo,key,index)=>(memo[key.name] = matches[index + 1],memo), {});
    console.log('bingo', this.params);
    return true;
  }

  if (this.path == pathname){
       return true;
  }

  if (!this.route){
      if(this.path == '/') { 
          return true;
      }
      
      return pathname.startsWith(this.path + '/');
  }
  return false;
}

Layer.prototype.handle_request = function(req, res, next){
  return this.handler(req,res,next)
}

module.exports = Layer;

lib/router/index.js
// ...

Router.prototype.handle = function (req, res, done) {
  const { pathname } = url.parse(req.url);
  const method = req.method.toLowerCase();
  let i = 0
  const next = (err) => {
    if (i == this.stack.length) return done();
    let layer = this.stack[i++];
    if (err) {
      if (!layer.route) {
        if (layer.handler.length === 4) {
          layer.handler(err, req, res, next);
        } else {
          next(err);
        }
      } else {
        next(err);
      }
    } else {
      if (layer.match(pathname)) { // layer 中用来管理自己的匹配逻辑
        req.params = layer.params; // 挂载正则匹配的参数

        if (!layer.route) { 
          if (layer.handler.length === 4) { 
            next();
          } else {
            layer.handle_request(req, res, next)
          }
        } else {
          if (layer.route.methods[method]) {
            layer.handle_request(req, res, next);
          } else {
            next();
          }
        }
      } else {
        next();
      }
    }
  }
  next();
}
module.exports = Router

二级路由实现(难点)

传统我们书写路由,都会这么写:

const express = require('./myExpress');
const app = express();

app.get('/user/add', function(req, res) {
  req.end('user add');
});

// 重复的 /user 书写很麻烦,express 内置了二级路由来做抽离
app.get('/user/remove', function(req, res) {
  req.end('user add');
});


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

我们来尝试下 express 提供的二级路由:

- myExpress
	- lib
		- router 
			- index.js  // 路由系统
			- layer.js  // 存储路径 -> route.diapatch 方法的映射
			- route.js  // 存储请求 method、回调任务,并提供 diapatch 派发执行
		- application.js
		- express.js
	- index.js 
- routerConfig
	- user.js

新建 routerConifg/user.js

user.js
const express = require('express');
// 这也是 express 不能基于 class 实现的一个原因
// Router 技能作为构造函数,也能作为普通函数调用
const router = express.Router();

router.get('/add', function(req, res) {
  res.end('user add');
});

router.get('/remove', function(req, res) {
  res.end('user add');
});

module.exports = router;

修改 server.js

user.js
// const express = require('./myExpress');
const express = require('express');
const app = express();
const user = require('./routerConfig/user'); // 拿到 user 路由

app.use('/user', user);

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

写起来既清晰又明了,那么怎么实现呢?

流程如下所示:

  1. app.use('/user', user),user 能作为一个 callback,说明 express.Router() 方法调用会返回一个函数。
function(req, res, next) {}
  1. express.Router 使用的也是 Router 构造函数,如果直接返回一个函数,则会影响 new 调用的 this,所以我们考虑一个变种写法,实例属性挂在返回的 function 上,声明一个对象 proto 收集原型链上的属性,然后返回的 function 的原型链指向 proto,就实现了这个能 new 能执行的 Router 方法。
代码示例
// 变种前
function Router() {
  this.stack = [];

  // 兼容直接二级路由调用的方式,但这样会影响 new Router 返回的 this
  return function(req, res, next) {
    
  }
}

// new 调用时 原型链东西也将丢失
Router.prototype.xx = function() {}

// -----------------------------变种后--------------------------------
function Router() {
  let router = function(req, res, next) {
    
  }

  router.stack = [];
  router.__proto__ = proto; 

  // 兼容直接二级路由调用的方式,但这样会影响 new Router 返回的 this
  return router;
}

let proto = {};

proto.xx = function() {}

  1. 和路由中间件不同的是,route 中存储的 layer 也有二级路由的路径,请求到来时先匹配一级路由,匹配掉之后摘除一级路由路径,进入 route 中匹配二级路由,二级路由处理完毕后,重新拼接一级路由路径往下匹配。
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');

function Router() {
  let router = function(req, res, next) {
    // 二级路由也是请求来了从 route 中匹配 layer,用普通路由用的同一个方法哦
    // 不过二级路由匹,要卸下一级路由再去 route 中匹配哦
    router.handle(req, res, next);
  }

  router.stack = [];
  router.__proto__ = proto;

  // 兼容直接二级路由调用的方式
  // 能 new 能执行,返回的函数即是实例也是普通调用时 Router 的返回结果
  return router
}

let proto = {};

proto.route = function (path) {
  let route = new Route();
  let layer = new Layer(path, route.dispatch.bind(route));
  layer.route = route;

  this.stack.push(layer);

  return route;
}

proto.use = function (path) {
  let args = Array.from(arguments);
  let handlers = [];
  // 如果第一个参数不传,默认是 '/'
  if (typeof path === 'function') {
    path = '/';
    handlers = [...args]
  } else {
    handlers = args.slice(1)
  }

  handlers.forEach(handler => {
    let layer = new Layer(path, handler);
    layer.route = undefined; // 后面判断 如果layer上有route属性 说明是路由,没有说明是中间件
    this.stack.push(layer);
  })
}

methods.forEach(method => {
  proto[method] = function (path, handlers) { // 像路由的stack中添加
    // 之前是注册路由时调用的 app.get,handlers 被处理成数组
    // 但是二级路由时,我们使用的 express.Router().get 没处理
    // 这里为了兼容,不是数组则转成数组
    if (!Array.isArray(handlers)) {
      handlers = Array.from(arguments).slice(1);
    }

    let route = this.route(path); // 创建route,并返回route
    route[method](handlers)
  }
})

proto.handle = function (req, res, done) {
  // 要在路由的栈中查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
  const { pathname } = url.parse(req.url);
  const method = req.method.toLowerCase();
  console.log('111', pathname, this.stack);
  let i = 0;
  let removed = '';

  const next = (err) => {
    if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
    let layer = this.stack[i++];

    // 拼回 remove
    if (removed.length) {
      req.url = removed + req.url;
      removed = ''; // 从next方法出来的时候 需要增添前缀
    }

    if (err) {
      // 如果有错误就在栈中查找错误处理中间件,不是错误处理中间件的就不要执行了
      if (!layer.route) {
        // 四个参数代表是错误中间件
        if (layer.handler.length === 4) {
          layer.handler(err, req, res, next);
        } else {
          next(err); // 正常的中间件
        }
      } else {
        next(err); // 路由
      }
    } else {
      // 无论路由还是中间件 都得匹配路径,但是中间件不匹配方法
      if (layer.match(pathname)) { // layer 中用来管理自己的匹配逻辑
        req.params = layer.params; // 挂载正则匹配的参数
        
        if (!layer.route) { // 中间件不需要匹配方法
          if (layer.handler.length === 4) { // 如果正常情况下是不执行错误处理中间件的
            next();
          } else {
            // 匹配到中间件时,需要删除中间件 path 的前缀,如果是 /,则不删除
            // 然后进入 route 中查找,比如 /user/add -> /add,目的兼容二级路由
            if (layer.path !== '/') {
              removed = layer.path; // 保存原来的 path
              req.url = req.url.slice(layer.path.length);
            }

            layer.handle_request(req, res, next)
          }
        } else { // 路由需要匹配方法,在执行
          console.log(11);
          if (layer.route.methods[method]) {
            layer.handle_request(req, res, next); // route.dispatch
          } else {
            next();
          }
        }
      } else {
        next();
      }
    }
  }
  next();
}
module.exports = Router

lib/router/layer.js
const pathToRegExp = require('path-to-regexp');

// 存一个路径和 handler 方法的对象
function Layer(path,handler){
  this.path = path;
  this.keys = [];
  // 将请求路径转成用于匹配的正则,并记录出现的 keys
  this.regexp = pathToRegExp(this.path, this.keys); 
  // keys: [{ name: 'id' }, { name: 'name' } ]
  // regexp: /^\/user\/(?:([^\/]+?))\/(?:([^\/]+?))\/xxx\/?$/i 用于匹配真实 path
  this.handler = handler; 
}

Layer.prototype.match = function(pathname) {
  const matches = pathname.match(this.regexp); // [1] id  [2] name

  // 如果 matches 存在,说明是正则路由,在 layer 上挂载解析好的 params 属性
  if (matches){
    // { id: '1', name: ':ys' }
    this.params = this.keys.reduce((memo,key,index)=>(memo[key.name] = matches[index + 1],memo), {});
    // console.log('bingo', this.params);
    return true;
  }

  // console.log(pathname, this.regexp);
  // 无论中间件layer 还是路由layer  只要一样肯定匹配到
  if(this.path == pathname){
       return true;
  }
  // 如果是中间件 我们开头匹配就可以
  if(!this.route){
      if(this.path == '/'){ // 中间件路径是/ 表示可以匹配到
          return true;
      }
      // /user/info  /user/
      // 为了避免 /u 也被匹配,我们加 '/' 匹配
      return pathname.startsWith(this.path + '/');
  }
  return false;
}

// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
  return this.handler(req,res,next)
}

module.exports = Layer;

lib/router/route.js(该文件没修改)
const Layer = require('./layer');
const methods = require('methods')
function Route() {
  this.stack = [];
   // 用来标识route上包含哪些方法
   // 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
  this.methods = {};
}

// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function(req,res,out){
  let i = 0;
  let next = (err) =>{ // 内部路由抛出错误 我就将错误派发到外层处理
      if(err) return out(err);
      if(i === this.stack.length) return out();
      let layer = this.stack[i++];
      console.log(layer.method, req.method.toLowerCase());
      if(layer.method === req.method.toLowerCase()){
          layer.handler(req, res, next); // 用户注册的回调
      }else{
          next();
      }
  }
  next();
}

methods.forEach(method => {
  Route.prototype[method] = function (handlers) {
    handlers.forEach(handler => {
      // 这里路径其实没有用到,不一定是 /,随便传哦。
      let layer = new Layer('/', handler);
      layer.method = method;// 给每一层都添加一个方法
      this.methods[method] = true;
      this.stack.push(layer);
    })
  }
})

module.exports = Route;

lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
  // this.router = new Router();
}

// 懒加载路由
Application.prototype.lazy_route = function () {
  if (!this.router) {
    this.router = new Router();
  }
}

methods.forEach(method => {
  // app.get,app.post 等
  Application.prototype[method] = function (path, ...handlers) { 
    // 懒加载路由
    this.lazy_route();
    this.router[method](path, handlers); // 像路由系统中添加
  }
})

// app.use()
Application.prototype.use = function(path, ...handler) {
  this.lazy_route();
  this.router.use(...arguments);
}

// app.listen()
Application.prototype.listen = function (...args) { 
  const server = http.createServer((req, res) => {
    function done() {
      res.end(`Cannot ${req.method} ${req.url}`)
    }
    this.lazy_route();
    this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
  });

  server.listen(...args)
}
module.exports = Application

lib/express.js
const Application = require('./application');
const Router = require('./router');

function createApplication() {
  return new Application();
}

// 可 new,可直接调用的 Route
createApplication.Router = Router; 

module.exports = createApplication;

路由中的参数中间件(app.param)

express 提供了一个参数校验的中间件,它能针对请求的参数做特殊处理。

const express = require('./express');
const app = express();
const path = require('path');
const fs = require('fs')

app.param('id', function(req, res, next, value, key) {
    console.log(value);
    next();
})
app.param('id', function(req, res, next, value, key) {
    console.log(value);
    next();
})
app.param('name', function(req, res, next, value, key) {
    console.log(value);
    next();
})
app.param('name', function(req, res, next, value, key) {
    console.log(value);
    next();
})
app.get('/user/:id/:name/xxx', function(req, res) {
	res.end('user');
})
app.listen(3000, function() {
    console.log(`server start 3000`);
})

也就是说,比如我两个路由的处理都用到了相同格式的参数,我能把这个处理提取出来,作为一个中间件前置校验。

比如上例中,我们 id 注册了两个回调 [fn1, fn2],name 也注册了两个回调 [fn1, fn2],这时候我们之前收集到的 layer.keys 为 [{ name: 'id' }, { name: 'name' } ],遍历 keys,依次执行回调即可。

lib/application.js
// 参数处理中间件
Application.prototype.param = function(path, ...handler) {
  this.lazy_route();
  this.router.param(...arguments);
}

lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');

function Router() {
  let router = function (req, res, next) {
    // 二级路由也是请求来了从 route 中匹配 layer,用普通路由用的同一个方法哦
    // 不过二级路由匹,要卸下一级路由再去 route 中匹配哦
    router.handle(req, res, next);
  }

  router.events = {};
  router.stack = [];
  router.__proto__ = proto;

  // 兼容直接二级路由调用的方式
  // 能 new 能执行,返回的函数即是实例也是普通调用时 Router 的返回结果
  return router
}

// ...

proto.param = function (key, callback) {
  // 收集参数对应的 cbs,{ id: [], name: []}
  if (this.events[key]) {
    this.events[key].push(callback);
  } else {
    this.events[key] = [callback]
  }
}

// 遍历 layer.keys,依次执行参数的回调,最后执行路由回调
proto.handle_params = function (req, res, layer, out) {
  let keys = layer.keys;
  if (!keys || !keys.length) return out();
  // 获取 ['id', 'name'] keys 列表
  keys = keys.reduce((memo, current) => [...memo, current.name], []);
  
  let events = this.events;
  let i = 0;
  let index = 0;
  let key;
  let fns;
  const next = () => {
    if (keys.length === i) return out()
    key = keys[i++]
    fns = events[key]; // {id:[fn1,fn2],name:[fn1,fn2]}
    if (fns) {
      processCallback();
    } else {
      next();
    }
  }
  next();

  function processCallback() {
    let fn = fns[index++];
    if (fn) {
      fn(req, res, processCallback, layer.params[key], key)
    } else {
      index = 0;
      next(); // 如果到头了 就执行下一个
    }
  }
}

// ...

express 实现 res.send, res.sendFile 扩展方法

我们知道,res.end 只能传入 buffer 或者 strng,express 基于 res 作了扩展。

app.get('/user/:id/:name/xxx', function(req, res) {
    res.sendFile(path.resolve(__dirname,'note.md'));
    res.json({ a: 1 });
    res.send({ a: 2 });
})

可以看到,这个只是在 res 上使用中间件扩展了一个方法而已,就比如

app.use(function(req, res) {
	res.send = function() {
		// do something
	}
});

app.get('/user/:id/:name/xxx', function(req, res) {
	// res.send({});
    res.sendFile(path.resolve(__dirname, '6.server.js'));
})

因为该中间件需要在所有路由注册之前最先注册进去,我们考虑把它防止路由初始化之后,emm,就放懒加载路由内部吧。

lib/application.js
// ...
Application.prototype.lazy_route = function () {
  if (!this.router) {
    this.router = new Router();

    this.use((req, res, next) => {
      res.send = function (data) {
        if (typeof data == 'object') {
          res.end(JSON.stringify(data))
        } else if (typeof data == 'string' || Buffer.isBuffer(data)) {
          res.end(data)
        }
      }
      res.sendFile = function (filePath) {
        fs.createReadStream(filePath).pipe(res);
      }
      next();
    })
  }
}
// ...

源码汇总

- myExpress
	- lib
		- router 
			- index.js 
			- layer.js  
			- route.js 
		- application.js
		- express.js
	- index.js 
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');

function Router() {
  let router = function (req, res, next) {
    // 二级路由也是请求来了从 route 中匹配 layer,用普通路由用的同一个方法哦
    // 不过二级路由匹,要卸下一级路由再去 route 中匹配哦
    router.handle(req, res, next);
  }

  router.events = {};
  router.stack = [];
  router.__proto__ = proto;

  // 兼容直接二级路由调用的方式
  // 能 new 能执行,返回的函数即是实例也是普通调用时 Router 的返回结果
  return router
}

let proto = {};

proto.route = function (path) {
  let route = new Route();
  let layer = new Layer(path, route.dispatch.bind(route));
  layer.route = route;

  this.stack.push(layer);

  return route;
}

proto.use = function (path) {
  let args = Array.from(arguments);
  let handlers = [];
  // 如果第一个参数不传,默认是 '/'
  if (typeof path === 'function') {
    path = '/';
    handlers = [...args]
  } else {
    handlers = args.slice(1)
  }

  handlers.forEach(handler => {
    let layer = new Layer(path, handler);
    layer.route = undefined; // 后面判断 如果layer上有route属性 说明是路由,没有说明是中间件
    this.stack.push(layer);
  })
}

methods.forEach(method => {
  proto[method] = function (path, handlers) { // 像路由的stack中添加
    // 之前是注册路由时调用的 app.get,handlers 被处理成数组
    // 但是二级路由时,我们使用的 express.Router().get 没处理
    // 这里为了兼容,不是数组则转成数组
    if (!Array.isArray(handlers)) {
      handlers = Array.from(arguments).slice(1);
    }

    let route = this.route(path); // 创建route,并返回route
    route[method](handlers)
  }
})

proto.param = function (key, callback) {
  // 收集参数对应的 cbs,{ id: [], name: []}
  if (this.events[key]) {
    this.events[key].push(callback);
  } else {
    this.events[key] = [callback]
  }
}

// 遍历 layer.keys,依次执行参数的回调,最后执行路由回调
proto.handle_params = function (req, res, layer, out) {
  let keys = layer.keys;
  if (!keys || !keys.length) return out();
  // 获取 ['id', 'name'] keys 列表
  keys = keys.reduce((memo, current) => [...memo, current.name], []);

  let events = this.events;
  let i = 0;
  let index = 0;
  let key;
  let fns;
  const next = () => {
    if (keys.length === i) return out()
    key = keys[i++]
    fns = events[key]; // {id:[fn1,fn2],name:[fn1,fn2]}
    if (fns) {
      processCallback();
    } else {
      next();
    }
  }
  next();

  function processCallback() {
    let fn = fns[index++];
    if (fn) {
      fn(req, res, processCallback, layer.params[key], key)
    } else {
      index = 0;
      next(); // 如果到头了 就执行下一个
    }
  }
}

proto.handle = function (req, res, done) {
  // 要在路由的栈中查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
  const { pathname } = url.parse(req.url);
  const method = req.method.toLowerCase();
  let i = 0;
  let removed = '';

  const next = (err) => {
    if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
    let layer = this.stack[i++];

    // 拼回 remove
    if (removed.length) {
      req.url = removed + req.url;
      removed = ''; // 从next方法出来的时候 需要增添前缀
    }

    if (err) {
      // 如果有错误就在栈中查找错误处理中间件,不是错误处理中间件的就不要执行了
      if (!layer.route) {
        // 四个参数代表是错误中间件
        if (layer.handler.length === 4) {
          layer.handler(err, req, res, next);
        } else {
          next(err); // 正常的中间件
        }
      } else {
        next(err); // 路由
      }
    } else {
      // 无论路由还是中间件 都得匹配路径,但是中间件不匹配方法
      if (layer.match(pathname)) { // layer 中用来管理自己的匹配逻辑
        req.params = layer.params; // 挂载正则匹配的参数

        if (!layer.route) { // 中间件不需要匹配方法
          if (layer.handler.length === 4) { // 如果正常情况下是不执行错误处理中间件的
            next();
          } else {
            // 匹配到中间件时,需要删除中间件 path 的前缀,如果是 /,则不删除
            // 然后进入 route 中查找,比如 /user/add -> /add,目的兼容二级路由
            if (layer.path !== '/') {
              removed = layer.path; // 保存原来的 path
              req.url = req.url.slice(layer.path.length);
            }

            layer.handle_request(req, res, next)
          }
        } else { // 路由需要匹配方法,在执行
          if (layer.route.methods[method]) {
            // 执行路由回调之前,先执行 param 回调
            // app.param('id', cb), layer 上有 keys
            this.handle_params(req, res, layer, () => {
              layer.handle_request(req, res, next); // route.dispatch
            });
          } else {
            next();
          }
        }
      } else {
        next();
      }
    }
  }
  next();
}
module.exports = Router

lib/router/layer.js
const pathToRegExp = require('path-to-regexp');

// 存一个路径和 handler 方法的对象
function Layer(path,handler){
  this.path = path;
  this.keys = [];
  // 将请求路径转成用于匹配的正则,并记录出现的 keys
  this.regexp = pathToRegExp(this.path, this.keys); 
  // keys: [{ name: 'id' }, { name: 'name' } ]
  // regexp: /^\/user\/(?:([^\/]+?))\/(?:([^\/]+?))\/xxx\/?$/i 用于匹配真实 path
  this.handler = handler; 
}

Layer.prototype.match = function(pathname) {
  const matches = pathname.match(this.regexp); // [1] id  [2] name

  // 如果 matches 存在,说明是正则路由,在 layer 上挂载解析好的 params 属性
  if (matches){
    // { id: '1', name: ':ys' }
    this.params = this.keys.reduce((memo,key,index)=>(memo[key.name] = matches[index + 1],memo), {});
    // console.log('bingo', this.params);
    return true;
  }

  // console.log(pathname, this.regexp);
  // 无论中间件layer 还是路由layer  只要一样肯定匹配到
  if(this.path == pathname){
       return true;
  }
  // 如果是中间件 我们开头匹配就可以
  if(!this.route){
      if(this.path == '/'){ // 中间件路径是/ 表示可以匹配到
          return true;
      }
      // /user/info  /user/
      // 为了避免 /u 也被匹配,我们加 '/' 匹配
      return pathname.startsWith(this.path + '/');
  }
  return false;
}

// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
  return this.handler(req,res,next)
}

module.exports = Layer;


lib/router/route.js
const Layer = require('./layer');
const methods = require('methods')
function Route() {
  this.stack = [];
   // 用来标识route上包含哪些方法
   // 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
  this.methods = {};
}

// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function(req,res,out){
  let i = 0;
  let next = (err) =>{ // 内部路由抛出错误 我就将错误派发到外层处理
      if(err) return out(err);
      if(i === this.stack.length) return out();
      let layer = this.stack[i++];
      // console.log(layer.method, req.method.toLowerCase());
      if(layer.method === req.method.toLowerCase()){
          layer.handler(req, res, next); // 用户注册的回调
      }else{
          next();
      }
  }
  next();
}

methods.forEach(method => {
  Route.prototype[method] = function (handlers) {
    handlers.forEach(handler => {
      // 这里路径其实没有用到,不一定是 /,随便传哦。
      let layer = new Layer('/', handler);
      layer.method = method;// 给每一层都添加一个方法
      this.methods[method] = true;
      this.stack.push(layer);
    })
  }
})

module.exports = Route;

lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const fs = require('fs');
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
  // this.router = new Router();
}

// 懒加载路由
Application.prototype.lazy_route = function () {
  if (!this.router) {
    this.router = new Router();

    this.use((req, res, next) => {
      res.send = function (data) {
        if (typeof data == 'object') {
          res.end(JSON.stringify(data))
        } else if (typeof data == 'string' || Buffer.isBuffer(data)) {
          res.end(data)
        }
      }
      res.sendFile = function (filePath) {
        fs.createReadStream(filePath).pipe(res);
      }
      next();
    })
  }
}

methods.forEach(method => {
  // app.get,app.post 等
  Application.prototype[method] = function (path, ...handlers) {
    // 懒加载路由
    this.lazy_route();
    this.router[method](path, handlers); // 像路由系统中添加
  }
})

// 参数处理中间件
Application.prototype.param = function (path, ...handler) {
  this.lazy_route();
  this.router.param(...arguments);
}

// app.use()
Application.prototype.use = function (path, ...handler) {
  this.lazy_route();
  this.router.use(...arguments);
}

// app.listen()
Application.prototype.listen = function (...args) {
  const server = http.createServer((req, res) => {
    function done() {
      res.end(`Cannot ${req.method} ${req.url}`)
    }
    this.lazy_route();
    this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
  });

  server.listen(...args)
}
module.exports = Application

lib/express.js
const Application = require('./application');
const Router = require('./router');

function createApplication() {
  return new Application();
}

// 可 new,可直接调用的 Route
createApplication.Router = Router; 

module.exports = createApplication;

index.js
module.exports = require('./lib/express');