express 中间件的简单应用与实现

提到 express 就不得不提到中间件,接下来就简单的介绍一下 expres 中间件的简单应用与部分常用函数的实现。

1. express 中间件的简单应用

在日常项目的开发中,登录验证是一个非常常见的场景,这个时候 express 中间件就可以派上用场了。接下来分别使用原生 nodeexpress 中间件的方法实现简单的登录验证。

应用场景:在获取博客列表之前要进行登录验证,只有在登录状态下才可以获取博客列表。

原生 node.js 实现登录验证:

if(method === 'GET' && req.path === '/api/blog/list') {
  // 若req.session.username有值说明已经登录
  if (req.session.username){
    // 登录状态下可以获取博客列表
    获取博客列表
  }
}

express 中间件实现登录验证:

function loginCheck(req, res, next) {
    if (某个登录成功的条件) {
       next(); // 登录成功执行next
    } else {
       res.json({
         errno: -1,
         msg: 'login fail'
    })
    }
}
// 若在登录状态下,loginCheck会执行next函数,从而执行第二个中间件,获取博客列表。
app.get('/api/blog/list', loginCheck, (req, res, next) => {
    获得博客列表
})

虽然上面这个简单的例子中看上去似乎原生 node.js 要更简单一些,但是使用中间件的方式封装性更好,在一些比价复杂的项目中就能体现出优势了。

2. express 中 use, get, post, next, listen 等方法的实现

结合上面的流程图和最后给出的完整代码,简单说明一下实现的过程:

这里以 use (在实现思路上和 get, post 是一样的)为例说明:

  • 获取路径和中间件

    use 将用户调用时传入的参数传给 register 函数,register 函数将获取到参数后分离出路径和中间两部分,路径存放在 info.path 中,中间件存放在 info.stack 中。然后 register 函数将分离开的路径和中间件再返回给 useuse 拿到分离后的路径和中间件之后,会将其赋值给 constructor 函数中 this.routes.all

  • listen 方法的实现

    使用 express 时调用 app.listen(3000) 实际上不只是监听 3000 端口还创建了一个 http 服务,参数是一个回调函数,代码如下:

    listen(...args) {
      const server = http.createServer(this.callback());
      server.listen(...args);
    }
  • res.json 方法的实现

    在回调函数 callback 中首先实现一个 res.json 方法,实现方式也比较简单。

    1. 定义 resHeaderContent-typeapplication/json
    2. 执行res.end({JSON.stringify(data)}) 将用户传入的 data 对象转换为 JSON 字符串输出到屏幕上。
  • 获取需要执行的中间件

    这部分通过 match 函数实现。通过 req.urlreq.method.toLowerCase 来获取需要执行的中间件。中间实现的方法就有很多了,思路就是通过method 找出相应的路径和中间件,这个已经在 this.routes 中分类存储了。然后找出路径和中间件之后利用 indexOf 方法,

    if (url.indexOf(routeInfo.path) === 0) 如果 if 判断成立说明满足条件,那么需要执行的中间件也就找出来了。

  • app.next 方法的实现

    这部分通过 handle 函数实现。思路:定义一个 next 函数,在那些需要执行的中间件中,先取出第一个,如果不为空那么开始执行,三个参数分别为 req, res, next。后面需要执行的中间件到底执不执行取决于前一个中间件的程序中是否调用了 next 方法。代码如下:

     const next = () => {
          // shift 取得数组的第一项
          const middleware = stack.shift();
          if (middleware) {
            // 执行中间件函数
            middleware(req, res, next);
          }
        };
        // 定义完后立即执行
        next();

附上完整代码:

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

class LikeExpress {
  constructor() {
    // 存放路径和中间件,all中存放的是调用use传入的路径和中间件
    this.routes = {
      all: [],
      get: [],
      post: []
    }
  }

  // 分离出路径和中间件,info.path中存放路径,info.stack 中存放中间件
  register(path) {
    const info = {};
    if (typeof path === 'string') {
      info.path = path;
      // info.stack 存放所有的中间件
      // 如果第一个参数是路由在取中间件时就要从数组的第2个位置开始取
      // slice.call(arguments, 1) 的作用就是取arguments从第二个位置开始之后的所有元素都取出来并变成数组的形式。
      info.stack = slice.call(arguments, 1);
    } else {
      // 如果第一个参数不是一个路由,那么我们就假定第一个参数是一个根路由
      info.path = '/';
      info.stack = slice.call(arguments, 0);
    }
    return info;
  }

  use() {
    // 实际使用时,参数是通过use传递进来的
    // 将所有的参数传入到register函数中
    const info = this.register.apply(this, arguments);
    // info 是一个对象,info.path 中存放的是路径,info.stack 中存放的是中间件
    this.routes.all.push(info);

  }

  get() {
    const info = this.register.apply(this, arguments);
    this.routes.get.push(info);

  }

  post() {
    const info = this.register.apply(this, arguments);
    this.routes.post.push(info);
  }

  // 匹配use,get或post方法会执行的中间件
  match(method, url) {
    let stack = [];
    if (url === '/favicon.ico') {
      return stack;
    }
    // 获取routes
    let curRoutes = [];
    // concat 是数组中的一个方法,如果没有参数,那么会生成一个当前数组的副本并将其赋值给前面的变量,如果有参数会将参数加入到生成的副本的后面然后将其赋值给变量
    // 如果是use,那么就把use中的路径和中间列表复制到curRoutes中
    // 如果方法是get或post那么下面这句话,由于this.routes.all是undefined,所以会将当前curRoutes生成一个副本赋值给curRoutes,其实一点变化都没有
    curRoutes = curRoutes.concat(this.routes.all);
    // 如果是get或post,那么就把相应的路径和中间件复制到curRoutes中
    curRoutes = curRoutes.concat(this.routes[method]);

    curRoutes.forEach(routeInfo => {
      // url='/api/get-cookie' routeInfo.path='/'
      // url='/api/get-cookie' routeInfo.path='/api'
      // url='api/get-cookie' routeInfo.path='/api/get-cookie'
      if (url.indexOf(routeInfo.path) === 0) {
        // 匹配成功
        stack = stack.concat(routeInfo.stack);
      }
    });
    return stack;
  }

  // 核心的 next 机制
  handle(req, res, stack) {
    const next = () => {
      // 拿到第一个匹配的中间件
      // shift 取得数组的第一项
      const middleware = stack.shift();
      if (middleware) {
        // 执行中间件函数
        middleware(req, res, next);
      }
    };
    // 定义完后立即执行
    next();

  }


  // callback是一个(req, res) => {} 的函数,结合http-test中的代码去理解
  callback() {
    return (req, res) => {
      // res.json 是一个函数,在express中使用时传入一个对象即可在屏幕中显示出来,这里实现了这个功能
      res.json = (data) => {
        res.setHeader('Content-type', 'application/json');
        res.end(
          JSON.stringify(data)
        );
      };

      const url = req.url;
      const method = req.method.toLowerCase();
      // 找到需要执行的中间件(通过路径和method,有可能一个需要执行的中间件也没有)
      const resultList = this.match(method, url);
      this.handle(req, res, resultList);
    }
  }

  // express 中listen的作用不仅仅是监听端口,还要创建服务器
  listen(...args) {
    const server = http.createServer(this.callback());
    server.listen(...args);
  }
}

module.exports = () => {
  return new LikeExpress()
};

12-26 16:59