前言

ExpressNode.jsWeb 框架,与 Koa 的轻量相比,功能要更多一些,依然是当前使用最广泛的 Node.js 框架,本篇参考 Express 的核心逻辑来实现一个简易版,Express 源码较多,逻辑复杂,看一周可能也看不完,如果你已经使用过 Express,又想快速的了解 Express 常用功能的原理,那读这篇文章算往前迈一小步,也可以为读真正的源码做铺垫,本篇内容每部分代码较多,因为按照 Express 的封装思想很难拆分,所以建议以星号标注区域为主其他代码为辅。

搭建基本服务

下面我们使用 Express 来搭建一个最基本的服务,只有三行代码,只能访问不能响应。

/* 三行代码搭建的最基本服务 */
// 引入 Express
const express = require('express');

// 创建服务
const app = express();

// 监听服务
app.listen(3000);

从上面我们可以分析出,express 模块给我们提供了一个函数,调用后返回了一个函数或对象给上面有 listen 方法给我们创建了一个 http 服务,我们就按照官方的设计返回一个函数 app

/* 文件:express.js */
const http = require('http');

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {}

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

module.exports = createApplication;

我们创建一个模块 express.js,导出了 createApplication 函数并返回在内部创建 app 函数,createApplication 等于我们引入 Express 模块时所调用的那个函数,返回值就是我们接收的 app,在 createApplication 返回的 app 函数上挂载了静态方法 listen,用于帮助我们启动 http 服务。

createApplication 函数内我们使用引入的 http 模块创建了服务,并调用了创建服务 serverlisten 方法,将 app.listen 的所有参数传递进去,这就等于做了一层封装,将真正创建服务器的过程都包在了 app.listen 内部,我们自己封装的 Express 模块只有在调用导出函数并调用 app.listen 时才会真正的创建服务器和启动服务器,相当于将原生的两步合二为一。

路由的实现

Express 框架中有多个路由方法,方法名分别对应不同的请求方式,可以帮助我们匹配路径和请求,在完全匹配时执行路由内部的回调函数,目的是在不同路由不同请求方法的情况下让服务器做出不同的响应,路由的使用方式如下。

/* 路由的使用方式 */
// 引入 Express
const express = require('express');

// 创建服务
const app = express();

// 创建路由
app.get('/', function (req, res) {
  res.end('home');
});

app.post('/about', function (req, res) {
  res.end('about');
});

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

// 监听服务
app.listen(3000);

如果启动上面的服务,通过浏览器访问定义的路由时可以匹配到 app.getapp.postapp.all 并执行回调,但其实我们可以发现这些方法的名字是与请求类型严格对应的,不仅仅这几个,下面来看看实现路由的核心逻辑(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// ************************** 以下为新增代码 **************************
// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');
// ************************** 以上为新增代码 **************************

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
// ************************** 以下为新增代码 **************************
    // 获取方法名统一转换成小写
    const method = req.method.toLowerCase();

    // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
    const [reqPath, query = ''] = req.url.split('?');

    // 循环匹配路径
    for (let i = 0; i < app.routes.lenth; i++) {
      // 循环取得每一层
      const layer = app.routes[i];

      // 如果说路径和请求类型都能匹配,则执行该路由层的回调
      if ((reqPath === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
        return layer.hanlder(req, res);
      }
    }

    // 如果都没有匹配上,则响应错误信息
    res.end('CANNOT ' + req.method + ' ' + reqPath);
// ************************** 以上为新增代码 **************************
  }

// ************************** 以下为新增代码 **************************
  // 存储路由层的请求类型、路径和回调
  app.routes = [];

  // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');
// ************************** 以上为新增代码 **************************

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

module.exports = createApplication;

我们的逻辑大体可以分为两个部分,路由方法的创建以及路由的匹配,首先是路由方法的创建阶段,每一个方法的内部所做的事情就是将路由的路径、请求方式和回调函数作为对象的属性,并将对象存入一个数组中统一管理,所以我们创建了 app.routes 数组用来存储这些路由对象。

方法名对应请求类型,请类型有很多,我们不会一一的创建每一个方法,所以选择引入专门存储请求类型名称的 methods 模块,其实路由方法逻辑相同,我们封装了 createRouteMethod 方法用来生成不同路由方法的函数体,之所以这样做是因为有个特殊的路由方法 app.all,导致请求类型有差别,其他的可以从 methods 中取,app.all 我们定义类型为 all 通过 createRouteMethod 函数的参数传入。

接着就是循环 methods 调用 createRouteMethod 函数创建路由方法,并单独创建 app.all 方法。

路由匹配阶段实在函数 app 内完成的,因为启动服务接收到请求时会执行 createServer 中的回调,即执行 app,先通过原生自带的 req.method 取出请求方式并处理成小写,通过 req.path 取出完整路径并分成路由名和查询字符串两个部分。

循环 app.routes 用取到请求的类型和路由名称匹配,两者都相等则执行对应路由对象上的回调函数,在判断条件中,请求方式兼容了我们之前定义的 all,为了所有的请求类型只要路由匹配都可以执行 app.all 的回调,请求路径兼容了 *,因为如果某个路由方法定义的路径为 *,则任意路由都可以执行这个路由对象上的回调。

扩展请求对象属性

且在路由内部可以通过 req 访问一些原生没有的属性如 req.pathreq.queryreq.hostreq.params,这说明 Express 在实现的过程中对 req 进行了处理。

/* req 属性的使用 */
// 引入 Express
const express = require('express');

// 创建服务
const app = express();

// 创建路由
app.get('/', function (req, res) {
  console.log(req.path);
  console.log(req.query);
  console.log(req.host);
  res.end('home');
});

app.get('/about/:id/:name', function (req, res) {
  console.log(req.params);
  res.end('about');
});

// 监听服务
app.listen(3000);

在上面的使用中我们写了两个路由,分别打印了原生所不具备而 Express 帮我们处理并新增的属性,下面我们就来在之前自己实现的 express.js 的基础上增加这些属性(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');

// ************************** 以下为新增代码 **************************
const querystring = require('querystring');
// ************************** 以上为新增代码 **************************

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
    // 获取方法名统一转换成小写
    const method = req.method.toLowerCase();

    // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
    const [reqPath, query = ''] = req.url.split('?');

// *************************** 以下为修改代码 **************************
    // 将路径名赋值给 req.path
    req.path = reqPath;
    // 将查询字符串转换成对象赋值给 req.query
    req.query = querystring.parse(query);
    // 将主机名赋值给 req.host
    req.host = req.headers.host.split(':')[0];

    // 循环匹配路径
    for (let i = 0; i < app.routes.lenth; i++) {
      // 循环取得每一层
      const layer = app.routes[i];

      // 如果路由对象上存在正则说明存在路由参数,否则正常匹配路径和请求类型
      if (layer.regexp) {
        // 使用路径配置的正则匹配请求路径
        const result = pathname.match(layer.regexp);

        // 如果匹配到结果且请求方式匹配
        if (result && (method === layer.method || layer.method === 'all')) {
          // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
          req.params = layer.paramNames.reduce(function (memo, key, index) {
            memo[key] = result[index + 1];
            return memo;
          }, {});

          // 执行对应的回调
          return layer.hanlder(req, res);
        }
      } else {
        // 如果说路径和请求类型都能匹配,则执行该路由层的回调
        if ((reqPath === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
          return layer.hanlder(req, res);
        }
      }
// ************************** 以上为修改代码 **************************
    }

    // 如果都没有匹配上,则响应错误信息
    res.end('CANNOT ' + req.method + ' ' + reqPath);
  }

  // 存储路由层的请求类型、路径和回调
  app.routes = [];

  // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

// ************************** 以下为新增代码 **************************
      // 如果含有路由参数,如 /xxx/:aa/:bb
      // 取出路由参数的键 aa bb 存入数组并挂在路由对象上
      // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
      if (pathname.indexOf(':') !== -1) {
        const paramNames = []; // 存储路由参数

        // 将路由参数取出存入数组,并返回正则字符串
        const regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
          paramNames.push(attr);
          return '(\\w+)';
        });

        const regexp = new RegExp(regStr); // 生成正则类型
        layer.regexp = regexp; // 将正则挂在路由对象上
        layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
      }
// ************************** 以上为新增代码 **************************

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

module.exports = createApplication;

上面代码有些长,我们一点一点分析,首先是 req.path,就是我们浏览器地址栏里查询字符串前的路径,值其实就是我们之前从 req.url 中解构出来的 pathname,我们只需要将 pathname 赋值给 req.path 即可。

req.query 是浏览器地址栏的查询字符串传递的参数,就是我们从 req.url 解构出来的查询字符串,借助 querystring 模块将查询字符串处理成对象赋值给 req.query 即可。

req.host 是访问的主机名,请求头中的 host 包含了主机名和端口号,我们只要截取出前半部分赋值给 req.host 即可。

最复杂的是 req.params 的实现,大概分为两个步骤,首先是在路由方法创建时需要检查定义的路由是否含有路由参数,如果有则取出参数的键存入数组 paramNames 中,然后创建一个匹配路由参数的正则,通过 replace 实现正则字符串的创建,再通过 RegExp 构造函数来创建正则,并挂在路由对象上,之所以使用 replace 是因为创建的规则内的分组要和路由参数的个数是相同的,我们将这些逻辑完善进了 createRouteMethod 函数中。

实现响应方法 send 和 sendFile

之前的例子中我们都是用原生的 end 方法响应浏览器,我们知道 end 方法只能接收字符串和 Buffer 作为响应的值,非常不方便,其实在 Express 中封装了一个 send 方法挂在 res 对象下,可以接收数组、对象、字符串、Buffer、数字处理后响应给浏览器,在 Express 内部同样封装了一个 sendFile 方法用于读取请求的文件。

/* send 响应 */
// 引入 Express
const express = require('express');
const path = require('path');

// 创建服务
const app = express();

// 创建路由
app.get('/', function (req, res) {
  res.send({ name: 'panda', age: 28 });
});

app.get('/test.txt', function (req, res) {
  // 必须传入绝对路径
  res.sendFile(path.join(__dirname, req.path));
});

// 监听服务
app.listen(3000);

通过我们的分析,封装的 send 方法应该是将 end 不支持的类型数据转换成了字符串,在内部再次调用 end,而 sendFile 方法规定参数必须为绝对路径,内部实现应该是利用可读流读取文件内容相应给浏览器,下面是两个方法的实现(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');
const querystring = require('querystring');

// ************************** 以下为新增代码 **************************
const util = require('util');
const httpServer = require('_http_server'); // 存储 node 服务相关信息
const fs = require('fs');
// ************************** 以上为新增代码 **************************

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
    // 获取方法名统一转换成小写
    const method = req.method.toLowerCase();

    // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
    const [reqPath, query = ''] = req.url.split('?');

    // 将路径名赋值给 req.path
    req.path = reqPath;
    // 将查询字符串转换成对象赋值给 req.query
    req.query = querystring.parse(query);
    // 将主机名赋值给 req.host
    req.host = req.headers.host.split(':')[0];

// ************************** 以下为新增代码 **************************
    // 响应方法
    res.send = function (params) {
      // 设置响应头
      res.setHeader('Content-Type', 'text/plain;charset=utf8');

      // 检测传入值得数据类型
      switch (typeof params) {
        case 'object':
          res.setHeader('Content-Type', 'application/json;charset=utf8');

          // 将任意类型的对象转换成字符串
          params = util.inspect(params);
          break;
        case 'number':
          // 数字则直接取出状态吗对应的名字返回
          params = httpServer.STATUS_CODES[params];
          break;
        default:
          break;
      }

      // 响应
      res.end(params);
    }

    // 响应文件方法
    res.sendFile = function (pathname) {
      fs.createReadStream(pathname).pipe(res);
    }
// ************************** 以上为新增代码 **************************

    // 循环匹配路径
    for (let i = 0; i < app.routes.lenth; i++) {
      // 循环取得每一层
      const layer = app.routes[i];

      // 如果路由对象上存在正则说明存在路由参数,否则正常匹配路径和请求类型
      if (layer.regexp) {
        // 使用路径配置的正则匹配请求路径
        const result = reqPath.match(layer.regexp);

        // 如果匹配到结果且请求方式匹配
        if (result && (method === layer.method || layer.method === 'all')) {
          // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
          req.params = layer.paramNames.reduce(function (memo, key, index) {
            memo[key] = result[index + 1];
            return memo;
          }, {});

          // 执行对应的回调
          return layer.hanlder(req, res);
        }
      } else {
        // 如果说路径和请求类型都能匹配,则执行该路由层的回调
        if ((reqPath === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
          return layer.hanlder(req, res);
        }
      }
    }

      // 如果都没有匹配上,则响应错误信息
    res.end('CANNOT ' + req.method + ' ' + reqPath);
  }

  // 存储路由层的请求类型、路径和回调
  app.routes = [];

  // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

      // 如果含有路由参数,如 /xxx/:aa/:bb
      // 取出路由参数的键 aa bb 存入数组并挂在路由对象上
      // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
      if (pathname.indexOf(':') !== -1) {
        const paramNames = []; // 存储路由参数

        // 将路由参数取出存入数组,并返回正则字符串
        const regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
          paramNames.push(attr);
          return '(\\w+)';
        });

        const regexp = new RegExp(regStr); // 生成正则类型
        layer.regexp = regexp; // 将正则挂在路由对象上
        layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
      }

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

module.exports = createApplication;

有一点需要注意,在 Node 环境中想把任何对象类型转换成字符串应该使用 util.inspect 方法,而当 send 方法输入数字类型时,要返回对应状态码的名称,可通过 _http_server 模块的 STATUS_CODES 对象获取。

内置中间件的实现

Express 最大的特点就是中间件机制,中间件就是用来处理请求的函数,用来完成不同场景的请求处理,一个中间件处理完请求后可以再传递给下一个中间件,具有回调函数 next,不执行 next 则会卡在一个位置,调用 next 则继续向下传递。

/* use 的使用 */
// 引入 Express
const express = require('express');
const path = require('path');

// 创建服务
const app = express();

// 创建路由
app.use(function (req, res, next) {
  res.setHeader('Content-Type', 'text/html;charset=utf8');
  next();
});

// 创建路由
app.get('/', function (req, res) {
  res.send({ name: 'panda', age: 28 });
});

// 监听服务
app.listen(3000);

在上面代码中使用 use 方法执行了传入的回调函数,实现公共逻辑,起到了中间件的作用,调用回调参数的 next 方法向下继续执行,下面来实现 use 方法(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');
const querystring = require('querystring');
const util = require('util');
const httpServer = require('_http_server'); // 存储 node 服务相关信息
const fs = require('fs');

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
// ************************** 以下为修改代码 **************************
    // 循环匹配路径
    let index = 0;

    function next(err) {
      // 获取第一个回调函数
      const layer = app.routes[index++];

      if (layer) {
        // 将当前中间件函数的属性解构出来
        const { method, pathname, handler } = layer;

        if (err) { // 如果存在错误将错误交给错误处理中间件,否则
          if (method === 'middle', handle.length === 4) {
            return hanlder(err, req, res, next);
          } else {
            next(err);
          }
        } else { // 如果不存在错误则继续向下执行
          // 判断是中间件还是路由
          if (method === 'middle') {
            // 匹配路径判断
            if (pathname === '/' || pathname === req.path || req.path.startWidth(pathname)) {
              handler(req, res, next);
            } else {
              next();
            }
          } else {
            // 如果路由对象上存在正则说明存在路由参数,否则正常匹配路径和请求类型
            if (layer.regexp) {
              // 使用路径配置的正则匹配请求路径
              const result = req.path.match(layer.regexp);

              // 如果匹配到结果且请求方式匹配
              if (result && ( method === layer.method || layer.method === 'all')) {
                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                req.params = layer.paramNames.reduce(function (memo, key, index ) {
                  memo[key] = result[index + 1];
                  return memo;
                }, {});

                // 执行对应的回调
                return layer.hanlder(req, res);
              } else {
                next();
              }
            } else {
              // 如果说路径和请求类型都能匹配,则执行该路由层的回调
              if ((req.path === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
                return layer.hanlder(req, res);
              } else {
                next();
              }
            }
          }
        }
      } else {
        // 如果都没有匹配上,则响应错误信息
        res.end('CANNOT ' + req.method + ' ' req.path);
      }
    }

    next();
// ************************** 以上为修改代码 **************************
  }

// ************************** 以下为新增代码 **************************
  function init() {
    return function (req, res, next) {
      // 获取方法名统一转换成小写
      const method = req.method.toLowerCase();

      // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
      const [reqPath, query = ''] = req.url.split('?');

      // 将路径名赋值给 req.path
      req.path = reqPath;
      // 将查询字符串转换成对象赋值给 req.query
      req.query = querystring.parse(query);
      // 将主机名赋值给 req.host
      req.host = req.headers.host.split(':')[0];

      // 响应方法
      res.send = function (params) {
        // 设置响应头
        res.setHeader('Content-Type', 'text/plain;charset=utf8');

        // 检测传入值得数据类型
        switch (typeof params) {
          case 'object':
            res.setHeader('Content-Type', 'application/json;charset=utf8');

            // 将任意类型的对象转换成字符串
            params = util.inspect(params);
            break;
          case 'number':
            // 数字则直接取出状态吗对应的名字返回
            params = httpServer.STATUS_CODES[params];
            break;
          default:
            break;
        }

        // 响应
        res.end(params);
      }

      // 响应文件方法
      res.sendFile = function (pathname) {
        fs.createReadStream(pathname).pipe(res);
      }

      // 向下执行
      next();
    }
  }
// ************************** 以上为新增代码 **************************

  // 存储路由层的请求类型、路径和回调
  app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

      // 如果含有路由参数,如 /xxx/:aa/:bb
      // 取出路由参数的键 aa bb 存入数组并挂在路由对象上
      // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
// ************************** 以下为修改代码 **************************
      if (pathname.indexOf(':') !== -1 && pathname.method !== 'middle') {
// ************************** 以上为修改代码 **************************
        const paramNames = []; // 存储路由参数

        // 将路由参数取出存入数组,并返回正则字符串
        const regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
          paramNames.push(attr);
          return '(\\w+)';
        });

        const regexp = new RegExp(regStr); // 生成正则类型
        layer.regexp = regexp; // 将正则挂在路由对象上
        layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
      }

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');

// ************************** 以下为新增代码 **************************
  // 添加中间件方法
  app.use = function (pathname, handler) {
    // 处理没有传入路径的情况
    if (typeof handler !== 'function') {
      handler = pathname;
      pathname = '/';
    }

    // 生成函数并执行
    createRouteMethod('middle')(pathname, handler);
  }

  // 将初始逻辑作为中间件执行
  app.use(init());
// ************************** 以上为新增代码 **************************

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

module.exports = createApplication;

use 方法第一个参数为路径,与路由相同,不传默认为 /,如果不传所有的路径都会经过该中间件,如果传入指定的值,则匹配后的请求才会通过该中间件。

中间件的执行可能存在异步的情况,但之前匹配路径使用的是 for 循环同步匹配,我们将其修改为异步并把路由匹配的逻辑与中间件路径匹配的逻辑进行了整合,并创建了 use 方法,对是否传了第一个参数做了一个兼容,其他将带有请求方式、路径和回调的逻辑统一使用 createRouteMethod 方法创建,并传入 middle 类型,createRouteMethod 中路由参数匹配的逻辑对 middle 类型做了一个排除。

使用 Express 中间件调用 next 方法时,不传递参数和参数为 null 代表执行成功,如果传入了其他的参数,表示执行出错,会跳过所有正常的中间件和路由,直接交给错误处理中间件处理,并将 next 传入的参数作为错误处理中间件回调函数的第一个参数 err,后面三个参数分别为 reqresnext

代码种创建了 index 变量,默认调用了一次 next 方法,每次然后取出数组 app.routes 中的路由对象的回调函数执行,并在内部执行 handler,而 handler 回调中又调用了 next 方法,就这样将整个中间件和路由的回调串联起来。

我们发现在第一次调用 next 之前的所有逻辑,如给 req 添加属性,给 res 添加方法,都是公共逻辑,是任何中间件和路由在匹配之前都会执行的逻辑,我们既然有了中间件方法 app.user,可以将这些逻辑抽取出来作为一个单独的中间件回调函数执行,所以创建了 init 函数,内部返回了一个函数作为回调函数,形参为 reqresnext,并在init 调用返回的函数内部调用 next 向下执行。

内置模板引擎的实现

Express 框架中内置支持了 ejsjade 等模板,使用方法 “三部曲” 如下。

/* 模板的使用 */
// 引入 Express
const express = require('express');
const path = require('path');

// 创建服务
const app = express();

// 1、指定模板引擎,其实就是模板文件的后缀名
app.set('view engine', 'ejs');

// 2、指定模板的存放根目录
app.set('views', path.resolve(__dirname, 'views'));

// 3、如果要自定义模板后缀和函数的关系
app.engine('.html', require('./ejs').__express);

// 创建路由
app.get('/user', function (req, res) {
  //使用指定的模板引擎渲染 user 模板
  res.render('user', { title: '用户管理' });
});

// 监听服务
app.listen(3000);

上面将模板根目录设置为 views 文件夹,并规定了模板类型为 ejs,可以同时给多种模板设置,并不冲突,如果需要将其他后缀名的模板按照另一种模板的渲染引擎渲染则使用 app.engine 进行设置,下面看一下实现代码(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');
const querystring = require('querystring');
const util = require('util');
const httpServer = require('_http_server'); // 存储 node 服务相关信息
const fs = require('fs');

// ************************** 以下为新增代码 **************************
const path = require('path');
// ************************** 以上为新增代码 **************************

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
    // 循环匹配路径
    let index = 0;

    function next(err) {
      // 获取第一个回调函数
      const layer = app.routes[index++];

      if (layer) {
        // 将当前中间件函数的属性解构出来
        const { method, pathname, handler } = layer;

        if (err) { // 如果存在错误将错误交给错误处理中间件,否则
          if (method === 'middle', handle.length === 4) {
            return hanlder(err, req, res, next);
          } else {
            next(err);
          }
        } else { // 如果不存在错误则继续向下执行
          // 判断是中间件还是路由
          if (method === 'middle') {
              // 匹配路径判断
              if (pathname === '/' || pathname === req.path || req.path.startWidth(pathname)) {
                handler(req, res, next);
              } else {
                next();
              }
          } else {
            // 如果路由对象上存在正则说明存在路由参数,否则正常匹配路径和请求类型
            if (layer.regexp) {
              // 使用路径配置的正则匹配请求路径
              const result = req.path.match(layer.regexp);

              // 如果匹配到结果且请求方式匹配
              if (result && (method === layer.method || layer.method === 'all')) {
                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                req.params = layer.paramNames.reduce(function (memo, key, index) {
                  memo[key] = result[index + 1];
                  return memo;
                }, {});

                // 执行对应的回调
                return layer.hanlder(req, res);
              } else {
                next();
              }
            } else {
              // 如果说路径和请求类型都能匹配,则执行该路由层的回调
              if ((req.path === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
                return layer.hanlder(req, res);
              } else {
                next();
              }
            }
          }
        }
      } else {
        // 如果都没有匹配上,则响应错误信息
        res.end('CANNOT ' + req.method + ' ' + req.path);
      }
    }

    next();
  }

  function init() {
    return function (req, res, next) {
      // 获取方法名统一转换成小写
      const method = req.method.toLowerCase();

      // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
      const [reqPath, query = ''] = req.url.split('?');

      // 将路径名赋值给 req.path
      req.path = reqPath;
      // 将查询字符串转换成对象赋值给 req.query
      req.query = querystring.parse(query);
      // 将主机名赋值给 req.host
      req.host = req.headers.host.split(':')[0];

      // 响应方法
      res.send = function (params) {
        // 设置响应头
        res.setHeader('Content-Type', 'text/plain;charset=utf8');

        // 检测传入值得数据类型
        switch (typeof params) {
          case 'object':
            res.setHeader('Content-Type', 'application/json;charset=utf8');

            // 将任意类型的对象转换成字符串
            params = util.inspect(params);
            break;
          case 'number':
            // 数字则直接取出状态吗对应的名字返回
            params = httpServer.STATUS_CODES[params];
            break;
          default:
            break;
        }

        // 响应
        res.end(params);
      }

      // 响应文件方法
      res.sendFile = function (pathname) {
        fs.createReadStream(pathname).pipe(res);
      }

// ************************** 以下为新增代码 **************************
      // 模板渲染方法
      res.render = function (filename, data) {
        // 将文件名和模板路径拼接
        let filepath = path.join(app.get('views'), filename);

        // 获取扩展名
        let extname = path.extname(filename.split(path.sep).pop());

        // 如果没有扩展名,则使用默认的扩展名
        if (!extname) {
          extname = '.' + app.get('view engine')
          filepath += extname;
        }

        // 读取模板文件并使用渲染引擎相应给浏览器
        app.engines[extname](filepath, data, function (err, html) {
          res.setHeader('Content-Type', 'text/html;charset=utf8');
          res.end(html);
        });
      }
// ************************** 以上为新增代码 **************************

      // 向下执行
      next();
    }
  }

  // 存储路由层的请求类型、路径和回调
  app.routes = [];

  // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
// ************************** 以下为修改代码 **************************
      // 满足条件说明是取值方法
      if (method === 'get' && arguments.length === 1) {
        return app.settings[pathname];
      }
// ************************** 以上为修改代码 **************************

      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

      // 如果含有路由参数,如 /xxx/:aa/:bb
      // 取出路由参数的键 aa bb 存入数组并挂在路由对象上
      // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
      if (pathname.indexOf(':') !== -1 && pathname.method !== 'middle') {
        const paramNames = []; // 存储路由参数

        // 将路由参数取出存入数组,并返回正则字符串
        const regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
          paramNames.push(attr);
          return '(\\w+)';
        });

        const regexp = new RegExp(regStr); // 生成正则类型
        layer.regexp = regexp; // 将正则挂在路由对象上
        layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
      }

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');

  // 添加中间件方法
  app.use = function (pathname, handler) {
    // 处理没有传入路径的情况
    if (typeof handler !== 'function') {
      handler = pathname;
      pathname = '/';
    }

    // 生成函数并执行
    createRouteMethod('middle')(pathname, handler);
  }

  // 将初始逻辑作为中间件执行
  app.use(init());

// ************************** 以下为新增代码 **************************
  // 存储设置的对象
  app.setting ={};

  // 存储模板渲染方法
  app.engines = {};

  // 添加设置的方法
  app.set = function (key, value) {
    app.use[key] = value;
  }

  // 添加渲染引擎的方法
  app.engine = function (ext, renderFile) {
    app.engines[ext] = renderFile;
  }
// ************************** 以上为新增代码 **************************

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

module.exports = createApplication;

在上面新增代码中设置了两个缓存 settingsengines,前者用来存储模板相关的设置,如渲染成什么类型的文件、读取模板文件的根目录,后者用来存储渲染引擎,即渲染模板的方法,这所以设置这两个缓存对象是为了实现 Express 多种不同模板共存的功能,可以根据需要进行设置和使用,而设置的方法分别为 app.setapp.engine,有设置值的方法就应该有取值的方法,但是 app.get 方法已经被设置为路由方法了,为了语义我们在 app.get 方法逻辑中进行了兼容,当参数为 1 个时,从 settings 中取值并返回,否则执行添加路由方法的逻辑。

之前都是准备工作,在使用时无论是中间件还是路由中都是靠调用 res.render 方法并传入模板路径和渲染数据来真正实现渲染和响应的,render 方法是在 init 函数初始化时就挂在了 res 上,核心逻辑是取出传入的模板文件后缀名,如果存在则使用后缀名,将文件名与默认读取模板的文件夹路径拼接传递给设置的渲染引擎的渲染方法,如果不存在后缀名则默认拼接 .html 当作后缀名,再与默认读取模板路径进行拼接,在渲染函数的回调中将渲染引擎渲染的模板字符串响应给浏览器。

内置静态资源中间件的实现

Express 内部可以通过路由处理静态文件,但是如果可能请求多个文件不可能一个文件对应一个路由,因此 Express 内部实现了静态文件中间件,使用如下。

/* 静态文件中间件的使用 */
// 引入 Express
const express = require('express');
const path = require('path');

// 创建服务
const app = express();

// 使用处理静态文件中间件
app.use(express.static(path.resolve(__dirname, 'public')));

// 监听服务
app.listen(3000);

从上面使用可以看出,express.static 是一个函数,执行的时候传入了一个参数,为默认查找文件的根路径,而添加中间件的 app.use 方法传入的参数正好是回调函数,这说明 express.static 方法需要返回一个函数,形参为 reqresnext,通过调用方式我们能看出 static 是静态方法,挂在了模块返回的函数上,实现代码如下(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');
const querystring = require('querystring');
const util = require('util');
const httpServer = require('_http_server'); // 存储 node 服务相关信息
const fs = require('fs');
const path = require('path');

// ************************** 以下为新增代码 **************************
const mime = require('mime');
// ************************** 以上为新增代码 **************************

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
    // 循环匹配路径
    let index = 0;

    function next(err) {
      // 获取第一个回调函数
      const layer = app.routes[index++];

      if (layer) {
        // 将当前中间件函数的属性解构出来
        const { method, pathname, handler } = layer;

        if (err) { // 如果存在错误将错误交给错误处理中间件,否则
          if (method === 'middle', handle.length === 4) {
            return hanlder(err, req, res, next);
          } else {
            next(err);
          }
        } else { // 如果不存在错误则继续向下执行
          // 判断是中间件还是路由
          if (method === 'middle') {
            // 匹配路径判断
            if (pathname === '/' || pathname === req.path || req.path.startWidth(pathname)) {
              handler(req, res, next);
            } else {
              next();
            }
          } else {
            // 如果路由对象上存在正则说明存在路由参数,否则正常匹配路径和请求类型
            if (layer.regexp) {
              // 使用路径配置的正则匹配请求路径
              const result = req.path.match(layer.regexp);

              // 如果匹配到结果且请求方式匹配
              if (result && (method === layer.method || layer.method === 'all')) {
                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                req.params = layer.paramNames.reduce(function (memo, key, index) {
                  memo[key] = result[index + 1];
                  return memo;
                }, {});

                // 执行对应的回调
                return layer.hanlder(req, res);
              } else {
                next();
              }
            } else {
              // 如果说路径和请求类型都能匹配,则执行该路由层的回调
              if ((req.path === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
                return layer.hanlder(req, res);
              } else {
                next();
              }
            }
          }
        }
      } else {
        // 如果都没有匹配上,则响应错误信息
        res.end('CANNOT ' + req.method + ' ' + req.path);
      }
    }

    next();
  }

  function init() {
    return function (req, res, next) {
      // 获取方法名统一转换成小写
      const method = req.method.toLowerCase();

      // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
      const [reqPath, query = ''] = req.url.split('?');

      // 将路径名赋值给 req.path
      req.path = reqPath;
      // 将查询字符串转换成对象赋值给 req.query
      req.query = querystring.parse(query);
      // 将主机名赋值给 req.host
      req.host = req.headers.host.split(':')[0];

      // 响应方法
      res.send = function (params) {
        // 设置响应头
        res.setHeader('Content-Type', 'text/plain;charset=utf8');

        // 检测传入值得数据类型
        switch (typeof params) {
          case 'object':
            res.setHeader('Content-Type', 'application/json;charset=utf8');
            // 将任意类型的对象转换成字符串
            params = util.inspect(params);
            break;
          case 'number':
            // 数字则直接取出状态吗对应的名字返回
            params = httpServer.STATUS_CODES[params];
            break;
          default:
            break;
        }

        // 响应
        res.end(params);
      }

      // 响应文件方法
      res.sendFile = function (pathname) {
        fs.createReadStream(pathname).pipe(res);
      }

      // 模板渲染方法
      res.render = function (filename, data) {
        // 将文件名和模板路径拼接
        let filepath = path.join(app.get('views'), filename);

        // 获取扩展名
        let extname = path.extname(filename.split(path.sep).pop());

        // 如果没有扩展名,则使用默认的扩展名
        if (!extname) {
          extname = '.' + app.get('view engine')}
          filepath += extname;
        }

        // 读取模板文件并使用渲染引擎相应给浏览器
        app.engines[extname](filepath, data, function (err, html) {
          res.setHeader('Content-Type', 'text/html;charset=utf8');
          res.end(html);
        });
      }

      // 向下执行
      next();
    }
  }

  // 存储路由层的请求类型、路径和回调
  app.routes = [];

  // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
      // 满足条件说明是取值方法
      if (method === 'get' && arguments.length === 1) {
        return app.settings[pathname];
      }

      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

      // 如果含有路由参数,如 /xxx/:aa/:bb
      // 取出路由参数的键 aa bb 存入数组并挂在路由对象上
      // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
      if (pathname.indexOf(':') !== -1 && pathname.method !== 'middle') {
        const paramNames = []; // 存储路由参数

        // 将路由参数取出存入数组,并返回正则字符串
        const regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
          paramNames.push(attr);
          return '(\\w+)';
        });

        const regexp = new RegExp(regStr); // 生成正则类型
        layer.regexp = regexp; // 将正则挂在路由对象上
        layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
      }

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');

  // 添加中间件方法
  app.use = function (pathname, handler) {
    // 处理没有传入路径的情况
    if (typeof handler !== 'function') {
      handler = pathname;
      pathname = '/';
    }

    // 生成函数并执行
    createRouteMethod('middle')(pathname, handler);
  }

  // 将初始逻辑作为中间件执行
  app.use(init());

  // 存储设置的对象
  app.setting ={};

  // 存储模板渲染方法
  app.engines = {};

  // 添加设置的方法
  app.set = function (key, value) {
    app.use[key] = value;
  }

  // 添加渲染引擎的方法
  app.engine = function (ext, renderFile) {
    app.engines[ext] = renderFile;
  }

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

// ************************** 以下为新增代码 **************************
createApplication.static = function (staticRoot) {
  return function (req, res, next) {
    // 获取文件的完整路径
    const filename = path.join(staticRoot, req.path);

    // 如果没有权限就向下执行其他中间件,如果有权限读取文件并响应
    fs.access(filename, function (err) {
      if (err) {
        next();
      } else {
        // 设置响应头类型和响应文件内容
        res.setHeader('Content-Type', mime.getType() + ';charset=utf8');
        fs.createReadStream(filename).pipe(res);
      }
    });
  }
}
// ************************** 以上为新增代码 **************************

module.exports = createApplication;

这个方法的核心逻辑是获取文件的路径,检查文件的权限,如果没有权限,则调用 next 交给其他中间件,这里注意的是 err 错误对象不要传递给 next,因为后面的中间件还要执行,如果传递后会直接执行错误处理中间件,有权限的情况下就正常读取文件内容,给 Content-Type 响应头设置文件类型,并将文件的可读流通过 pipe 方法传递给可写流 res,即响应给浏览器。

实现重定向

Express 中有一个功能在我们匹配到的某一个路由中调用可以直接跳转到另一个路由,即 302 重定向。

/* 使用重定向 */
// 引入 Express
const express = require('express');
const path = require('path');

// 创建服务
const app = express();

// 创建路由
app.get('/user', function (req, res, next) {
  res.end('user');
});

app.get('/detail', function (req, res, next) {
  // 访问 /detail 重定向到 /user
  res.redirect('/user');
});

// 监听服务
app.listen(3000);

看到上面的使用方式,我们根据前面的套路知道是 Expressres 对象上给挂载了一个 redirect 方法,参数为状态码(可选)和要跳转路由的路径,并且这个方法应该在 init 函数调用时挂在 res 上的,下面是实现的代码(直接找到星号提示新增或修改位置即可)。

/* 文件:express.js */
const http = require('http');

// methods 模块返回存储所有请求方法名称的数组
const methods = require('methods');
const querystring = require('querystring');
const util = require('util');
const httpServer = require('_http_server'); // 存储 node 服务相关信息
const fs = require('fs');
const path = require('path');
const mime = require('mime');

function createApplication() {
  // 创建 app 函数,身份类似 “总管家”,用于将请求分派给别人处理
  const app = function (req, res) {
    // 循环匹配路径
    let index = 0;

    function next(err) {
      // 获取第一个回调函数
      const layer = app.routes[index++];

      if (layer) {
        // 将当前中间件函数的属性解构出来
        const { method, pathname, handler } = layer;

        if (err) { // 如果存在错误将错误交给错误处理中间件,否则
          if (method === 'middle', handle.length === 4) {
            return hanlder(err, req, res, next);
          } else {
            next(err);
          }
        } else { // 如果不存在错误则继续向下执行
          // 判断是中间件还是路由
          if (method === 'middle') {
            // 匹配路径判断
            if (pathname === '/' || pathname === req.path || req.path.startWidth(pathname)) {
              handler(req, res, next);
            } else {
              next();
            }
          } else {
            // 如果路由对象上存在正则说明存在路由参数,否则正常匹配路径和请求类型
            if (layer.regexp) {
              // 使用路径配置的正则匹配请求路径
              const result = req.path.match(layer.regexp);

              // 如果匹配到结果且请求方式匹配
              if (result && (method === layer.method || layer.method === 'all')) {
                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                req.params = layer.paramNames.reduce(function (memo, key, index) {
                  memo[key] = result[index + 1];
                  return memo;
                }, {});

                // 执行对应的回调
                return layer.hanlder(req, res);
              } else {
                next();
              }
            } else {
              // 如果说路径和请求类型都能匹配,则执行该路由层的回调
              if ((req.path === layer.pathname || layer.pathname === '*') && (method === layer.method || layer.method === 'all')) {
                return layer.hanlder(req, res);
              } else {
                next();
              }
            }
          }
        }
      } else {
        // 如果都没有匹配上,则响应错误信息
        res.end('CANNOT ' + req.method} + '' + req.path);
      }
    }

    next();
  }

  function init() {
    return function (req, res, next) {
      // 获取方法名统一转换成小写
      const method = req.method.toLowerCase();

      // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
      const [reqPath, query = ''] = req.url.split('?');

      // 将路径名赋值给 req.path
      req.path = reqPath;
      // 将查询字符串转换成对象赋值给 req.query
      req.query = querystring.parse(query);
      // 将主机名赋值给 req.host
      req.host = req.headers.host.split(':')[0];

      // 响应方法
      res.send = function (params) {
        // 设置响应头
        res.setHeader('Content-Type', 'text/plain;charset=utf8');

        // 检测传入值得数据类型
        switch (typeof params) {
          case 'object':
            res.setHeader('Content-Type', 'application/json;charset=utf8');

            // 将任意类型的对象转换成字符串
            params = util.inspect(params);
            break;
          case 'number':
            // 数字则直接取出状态吗对应的名字返回
            params = httpServer.STATUS_CODES[params];
            break;
          default:
            break;
        }

        // 响应
        res.end(params);
      }

      // 响应文件方法
      res.sendFile = function (pathname) {
        fs.createReadStream(pathname).pipe(res);
      }

      // 模板渲染方法
      res.render = function (filename, data) {
        // 将文件名和模板路径拼接
        let filepath = path.join(app.get('views'), filename);

        // 获取扩展名
        let extname = path.extname(filename.split(path.sep).pop());

        // 如果没有扩展名,则使用默认的扩展名
        if (!extname) {
          extname = '.' + app.get('view engine')
          filepath += extname;
        }

        // 读取模板文件并使用渲染引擎相应给浏览器
        app.engines[extname](filepath, data, function (err, html) {
          res.setHeader('Content-Type', 'text/html;charset=utf8');
          res.end(html);
        });
      }

// ************************** 以下为新增代码 **************************
      // 重定向方法
      res.redirect = function (status, target) {
        // 如果第一个参数是字符串类型说明没有传状态码
        if (typeof status === 'string') {
          // 将第二个参数(重定向的目标路径)设置给 target
          target = status;

          // 再把状态码设置成 302
          status = 302;
        }

        // 响应状态码,设置重定向响应头
        res.statusCode = status;
        res.setHeader('Location', target);
        res.end();
      }
// ************************** 以上为新增代码 **************************

      // 向下执行
      next();
    }
  }

  // 存储路由层的请求类型、路径和回调
  app.routes = [];

  // 返回一个函数体用于将路由层存入 app.routes 中
  function createRouteMethod(method) {
    return function (pathname, handler) {
      // 满足条件说明是取值方法
      if (method === 'get' && arguments.length === 1) {
        return app.settings[pathname];
      }

      const layer = {
        method,
        pathname, // 不包含查询字符串
        handler
      };

      // 如果含有路由参数,如 /xxx/:aa/:bb
      // 取出路由参数的键 aa bb 存入数组并挂在路由对象上
      // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
      if (pathname.indexOf(':') !== -1 && pathname.method !== 'middle') {
        const paramNames = []; // 存储路由参数

        // 将路由参数取出存入数组,并返回正则字符串
        const regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
          paramNames.push(attr);
          return '(\\w+)';
        });

        const regexp = new RegExp(regStr); // 生成正则类型
        layer.regexp = regexp; // 将正则挂在路由对象上
        layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
      }

      // 把这一层放入存储所有路由层信息的数组中
      app.routes.push(layer);
    }
  }

  // 循环构建所有路由方法,如 app.get app.post 等
  methods.forEach(function (method) {
    // 匹配路由的 get 方法
    app[method] = createRouteMethod(method);
  });

  // all 方法,通吃所有请求类型
  app.all = createRouteMethod('all');

  // 添加中间件方法
  app.use = function (pathname, handler) {
    // 处理没有传入路径的情况
    if (typeof handler !== 'function') {
      handler = pathname;
      pathname = '/';
    }

    // 生成函数并执行
    createRouteMethod('middle')(pathname, handler);
  }

  // 将初始逻辑作为中间件执行
  app.use(init());

  // 存储设置的对象
  app.setting ={};

  // 存储模板渲染方法
  app.engines = {};

  // 添加设置的方法
  app.set = function (key, value) {
    app.use[key] = value;
  }

  // 添加渲染引擎的方法
  app.engine = function (ext, renderFile) {
    app.engines[ext] = renderFile;
  }

  // 启动服务的 listen 方法
  app.listen = function () {
    // 创建服务器
    const server = http.createServer(app);

    // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
    server.listen(...arguments);
  }

  // 返回 app
  return app;
}

createApplication.static = function (staticRoot) {
  return function (req, res, next) {
    // 获取文件的完整路径
    const filename = path.join(staticRoot, req.path);

    // 如果没有权限就向下执行其他中间件,如果有权限读取文件并响应
    fs.access(filename, function (err) {
      if (err) {
        next();
      } else {
        // 设置响应头类型和响应文件内容
        res.setHeader('Content-Type', mime.getType() + ';charset=utf8');
        fs.createReadStream(filename).pipe(res);
      }
    });
  }
}

module.exports = createApplication;

其实 res.redirect 方法的核心逻辑就是处理参数,如果没有传状态码的时候将参数设置给 target,将状态码设置为 302,并设置重定向响应头 Location

总结

到此为止 Express 的大部分内置功能就都简易的实现了,由于 Express 内部的封装思想,以及代码复杂、紧密的特点,各个功能代码很难单独拆分,总结一下就是很难表述清楚,只能通过大量代码来堆砌,好在每一部分实现我都标记了 “重点”,但看的时候还是要经历 “痛苦”,这已经将 Express 中的逻辑 “阉割” 到了一定的程度,读 Express 的源码一定比读这篇文章更需要耐心,当然如果你已经读到了这里证明困难都被克服了,继续加油。