美文网首页
Express 的实现(超详细)

Express 的实现(超详细)

作者: 六六_9d0d | 来源:发表于2019-09-30 14:22 被阅读0次

    原文链接:https://segmentfault.com/a/1190000019607502

    序:

    因为公司 Node 方面业务都是基于一个小型框架写的,这个框架是公司之前的一位同事根据 Express 的中间件思想写的一个小型 Socket 框架,阅读其源码之后,对 Express 的中间件思想有了更深入的了解,接下来就手写一个 Express 框架 ,以作为学习的产出 。

    在阅读了同事的代码与 Express 源码之后,发现其实 Express 的核心就是中间件的思想,其次是封装了更丰富的 API 供我们使用,废话不多说,让我们来一步一步实现一个可用的 Express

    本文的目的在于验证学习的收获,大概细致划分如下:

    • 服务器监听的原理
    • 路由的解析与匹配
    • 中间件的定义与使用
    • 核心 next() 方法
    • 错误处理中间件定义与使用
    • 内置 API 的封装

    正文:

    在手写框架之前,我们有必要去回顾一下 Express 的简单使用,从而对照它给我们提供的 API 去实现其相应的功能:

    新建一个 app.js 文件,添加如下代码:

    // app.js
    
    let express = require('express');
    
    let app = express();
    
    app.listen(3000, function () {
      console.log('listen 3000 port ...')
    })
    

    现在,在命令行中执行:

    node app.js
    
    1.png

    可以看到,程序已经在我们的后台跑起来了。

    当我们为其添加一个路由:

    let express = require('Express');
    
    let app = express();
    
    app.get('/hello', function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
    
      res.end('我是新添加的路由,只有 get 方法才可以访问到我 ~')
    })
    
    app.listen(3000, function () {
      console.log('listen 3000 port ...')
    })
    

    再次重启:在命令行中执行启动命令:(每次修改代码都需要重新执行脚本)并访问浏览器本地 3000 端口:

    2.png

    这里的乱码是因为:服务器不知道你要怎样去解析输出,所以我们需要指定响应头:

    let express = require('Express');
    
    let app = express();
    
    app.get('/hello', function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=utf-8') // 指定 utf-8 
      res.end('我是新添加的路由,只有 get 方法才可以访问到我 ~')
    })
    app.post('/hi', function (req, res) {
      res.end('我是新添加的路由,只有 post 方法才可以访问到我 ~')
    })
    
    app.listen(3000, function () {
      console.log('listen 3000 port ...')
    })
    
    3.png

    我们先来实现上面的功能:

    1. 服务器监听的原理

    新建一个 MyExpress.js,定义一个入口函数:

    let http = require('http');
    
    function createApplication () {
      // 定义入口函数,初始化操作
      let app = function (req, res) {
    
      }
      // 定义监听方法
      app.listen = function () {
        // 通过 http 模块创建一个服务器实例,该实例的参数是一个函数,该函数有两个参数,分别是 req 请求对象和 res 响应对象
        let server = http.createServer(app); 
        // 将参数列表传入,为实例监听配置项
        server.listen(...arguments); 
      }
      // 返回该函数
      return app
    }
    
    module.exports = createApplication;
    

    现在,我们代码中的 app.listen() 其实就已经实现了,可以将引入的 express
    替换为我们写的 MyExpress 做验证:

    let express = require('Express');
    // 替换为
    let express = require('./MyExpress');
    

    2. 路由的解析与匹配:

    接下来,

    我们先看看 routes 中的原理图

    4.png

    根据上图,路由数组中存在多个 layer 层,每个 layer 中包含了三个属性, methodpathhandler 分别对应请求的方式、请求的路径、执行的回调函数,代码如下:

    
    const http = require('http')
    
    function createApp () {
      let app = function (req, res) {
    
      };
    
      app.routes = []; // 定义路由数组
    
      let methods = http.METHODS; // 获取所有请求方法,比如常见的 GET/POST/DELETE/PUT ...
      methods.forEach(method => {
        method = method.toLocaleLowerCase() // 小写转换
        app[method] = function (path, handler) {
          let layer = {
            method,
            path,
            handler,
          }
          // 将每一个请求保存到路由数组中
          app.routes.push(layer)
        }
      })
    
      // 定义监听的方法
      app.listen = function () {
        let server = http.createServer(app);
        server.listen(...arguments)
      }
    
      return app;
    }
    
    module.exports = createApp
    

    到这里,仔细思考下,当脚本启动时,我们把所有的路由都保存到了 routes,打印 routes ,可以看到:

    5.png

    是不是和我们上面图中的一模一样 ~

    此时,我们访问对应的路径,发现浏览器一直转圈圈这是因为我们只是完成了存的操作,把所有的 layer 层存到了 routes

    那么我们该如何才可以做的当访问的时候,调用对应的 handle 函数呢?

    思路:当我们访问路径时,也就是获取到请求对象 req 时,我们需要遍历所存入的 layer 与访问的 methodpath 进行匹配,匹配成功,则执行对应的 handler 函数

    代码如下:

    const url = require('url')
    ......
    let app = function (req, res) {
      let reqMethod = req.method.toLocaleLowerCase() // 获取请求方法
      let pathName = url.parse(req.url, true).pathname // 获取请求路径
      console.log(app.routes);
      app.routes.forEach(layer => {
        let { method, path, handler } = layer;
        if (method === reqMethod && path === pathName) {
          handler(req, res)
        }
      });
    };
    ......
    
    

    至此,路由的定义与解析也基本完成。

    3. 中间件的定义与使用

    接下来,就是重点了,中间件思想

    中间件的定义其实与路由的定义差不多,也是存在 routes 中,但是,必须放到所有路由的 layer 之前,原理如下图:

    6.png

    其中,middle1middle2middle3 都是中间件,middle3 放在最后面,一般作为错误处理中间件,并且,每次访问服务器的时候,所有的请求先要经过 middle1middle2 做处理。

    在中间件中,有一个 next 方法,其实 next 方法就是使 ** layer** 的 index 标志向后移一位,并进行匹配,匹配成功执行回调,匹配失败则继续向后匹配,有点像 ** 回调队列**。

    核心 next() 方法

    接下来我们实现一个 next 方法:

    因为只有中间件的回调中才具有 next 方法,但是我们的中间件和路由的 layer 层都是存在 routes 中的,所以首先要判断 layer 中的 method 是否为 middle 初次之外,还要判断,中间件的路由是否相匹配,因为有些中间件是针对某个路由的。

    let reqMethod = req.method.toLocaleLowerCase()
    let pathName = url.parse(req.url, true).pathname
    let index = 0;
    function next () {
      // 中间件处理
      if (method === 'middle') {
        // 检测 path 是否匹配
        if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
          handler(req, res, next) // 执行中间件回调
        } else {
          next()
        }
        // 路由处理
      } else {
        // 检测 method 与 path 是否匹配
        if (method === reqMethod && path === pathName) {
          handler(req, res) // 执行路由回调
        } else {
          next()
        }
      }
    }
    
    next() // 这里必须要调用一次 next ,意义在于初始化的时候,取到第一个 layer,
    

    如果遍历完 routes,都没有匹配的 layer,该怎么办呢?所以要在 next 方法最先判断是否边已经遍历完:

    function next () {
      // 判断是否遍历完
      if (app.routes.length === index) {
        return res.end(`Cannot ${reqMethod} ${pathName}`)
      }
      let { method, path, handler } = app.routes[index++];
      // 中间件处理
      if (method === 'middle') {
        if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
          handler(req, res, next)
        } else {
          next()
        }
      } else {
        // 路由处理
        if (method === reqMethod && path === pathName) {
          handler(req, res)
        } else {
          next()
        }
      }
    }
    next()
    

    这样,一个 next 方法功能基本完成了。

    4. 错误处理中间件定义与使用

    如上面图中所示,错误处理中间件放在最后,就像一个流水线工厂,错误处理就是最后一道工序,但并不是所有的产品都需要跑最后一道工序,就像:只有不合格的产品,才会进入最后一道工序,并被贴上不合格的标签,以及不合格的原因。

    我们先看看 Express 中的错误是怎么被处理的:

    // 中间件1
    app.use(function (req, res, next) {
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
      console.log('middle1')
      next('这是错误')
    })
    // 中间件2
    app.use(function (req, res, next) {
      console.log('middle2')
      next()
    })
    // 中间件3(错误处理)
    app.use(function (err, req, res, next) {
      if (err) {
        res.end(err)
      }
      next()
    })
    

    如上图所示:有三个中间件,当 next 方法中抛出错误时,会把错误当做参数传入 next 方法,然后,next 指向的下一个方法就是错误处理的回调函数,也就是说:next 方法中的参被当做了错误处理中间件的 handler 函数的参数传入。代码如下:

    function next (err) {
      // 判断是否遍历完成
      if (app.routes.length === index) {
        return res.end(`Cannot ${reqMethod} ${pathName}`)
      }
      let { method, path, handler } = app.routes[index++];
      if (err) {
        console.log(handler.length)
        // 判断是否有 4 个参数:因为错误中间件与普通中间件最直观的区别就是参数数量不同
        if (handler.length === 4) {
          // 错误处理回调
          handler(err, req, res, next)
        } else {
          // 一直向下传递
          next(err)
        }
      } else {
          // 中间件处理
          if (method === 'middle') {
            if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
              handler(req, res, next)
            } else {
              next()
            }
          } else {
            // 路由处理
            if (method === reqMethod && path === pathName) {
              handler(req, res)
            } else {
              next()
            }
          }
      }
    }
    

    麻雀虽小五脏俱全,至此,一个简单的 Express 就完成了。你可以根据自己的兴趣来封装自己的 API 了 ...

    总结:

    1. 中间件的核心是 next 方法。
    2. next 方法只负责维护 routes 数组和取出 layer,根据条件去决定是否执行回调。

    附完整代码:

    const http = require('http')
    const url = require('url')
    
    
    function createApp () {
    
      let app = function (req, res) {
    
        let reqMethod = req.method.toLocaleLowerCase()
        let pathName = url.parse(req.url, true).pathname
        let index = 0;
        
        function next (err) {
    
          if (app.routes.length === index) {
            return res.end(`Cannot ${reqMethod} ${pathName}`)
          }
    
          let { method, path, handler } = app.routes[index++];
          if (err) {
            console.log(handler.length)
            if (handler.length === 4) {
              console.log(1)
              handler(err, req, res, next)
            } else {
              next(err)
            }
          } else {
              if (method === 'middle') {
                if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
                  handler(req, res, next)
                } else {
                  next()
                }
              } else {
                if (method === reqMethod && path === pathName) {
                  handler(req, res)
                } else {
                  next()
                }
              }
          }
        }
    
        next()
    
      };
    
      let methods = http.METHODS;
      app.routes = [];
      methods.forEach(method => {
        method = method.toLocaleLowerCase()
        app[method] = function (path, handler) {
          let layer = {
            method,
            path,
            handler,
          }
          app.routes.push(layer)
        }
      })
    
      app.use = function (path, handler) {
        if (typeof path === 'function') {
          handler = path;
          path = '/';
        }
        let layer = {
          method: 'middle',
          handler,
          path
        }
        app.routes.push(layer)
      }
    
      app.listen = function () {
        let server = http.createServer(app);
        server.listen(...arguments)
      }
    
      return app;
    
    }
    
    module.exports = createApp
    

    相关文章

      网友评论

          本文标题:Express 的实现(超详细)

          本文链接:https://www.haomeiwen.com/subject/ytejuctx.html