手写一个 Express(初级版)

栏目: Node.js · 发布时间: 4年前

内容简介:因为公司在阅读了同事的代码与本文的目的在于验证学习的收获,大概细致划分如下:

序:

因为公司 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

手写一个 Express(初级版)

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

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

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 端口:

手写一个 Express(初级版)

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

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 ...')
})

手写一个 Express(初级版)

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

新建一个 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');

接下来,我们先看看 routes 中的原理图:

手写一个 Express(初级版)

根据上图,路由数组中存在多个 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 ,可以看到:

手写一个 Express(初级版)

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

此时, 我们访问对应的路径,发现浏览器一直转圈圈 这是因为我们只是完成了存的操作,把所有的 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)
    }
  });
};
......

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

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

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

手写一个 Express(初级版)

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

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

接下来我们实现一个 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 方法功能基本完成了。

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

我们先看看 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 就完成了。

总结:

  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(初级版)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具