nodejs 中间件实现原理浅析

243 阅读2分钟

dianzang.png

概念

中间件是处在HTTP的Request和Response中间,用来实现某种中间功能的函数。例如IP筛选,查询字符串传递,请求体解析,cookie信息处理,权限校验,日志记录,会话管理中间件(session),gzip压缩中间件(如compress),错误处理,这样与业务本身关联不强,却又需要公共抽象的模块。

从头开始构建一个中间件

开发准备

热更新

使用nodemon监听文件的变化热更新提升开发体验。稍微设置一下延迟时间,减少无效保存操作影响。

"dev":"nodemon --delay 250ms ./app.js",

debug

基于vscode 调试工具进行开发提效

debug.png

正式开发

构建一个nodejs的http服务

const http = require('http')
const port = 9527
// http服务封装类
class Middleware {
  constructor() {}
  listen(port) {
    this.httpModel = http.createServer((req, res) => {
        res.end("hello world")
    }))
    this.httpModel.listen(port)
  }
}
// 实例化对象
const app = new Middleware()
// 启动监听
app.listen(port)

常见的express和koa都是采用类似的方式实例化的可以查看下面的代码示例,不仅仅是应该开源团队成员的原因,实际上本身http模块就是使用这样的形式去创建服务的。

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

构建路由

路由的实际本身其实就是一个map映射表,对应的路径匹配可以执行的函数。实现起来还是比较简单。

const http = require('http')
const port = 9527
// http服务封装类
class Middleware {
  constructor() {}
  // 关系存储
  getRouters = new Map()
  // 监听端口方法
  listen(port) {
    this.httpModel = http.createServer((req, res) => {
       // 如果是get请求
       if (req.method === 'GET') {
            // 获取map关系 
            if (this.getRouters.get(req.url)) {
              // 执行回调
              this.getRouters.get(req.url)(req, res)
            } else {
              res.end()
            }
          } else {
          }
       }
    }))
    this.httpModel.listen(port)
  },
  // get请求map关系建立
  get(url, callback) {
    this.getRouters.set(url, callback)
  }
}
// 实例化对象
const app = new Middleware()
// 拦截请求
app.get('/api/getUser', (req, res) => {
  res.end(JSON.stringify({ name: 'wuwenzhou' }))
})
// 启动监听
app.listen(port)

中间件

概念

中间件可以说是面向切片的最佳实践了,这种在运行时动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程,在不改变原有业务行为的前提下,扩展了本身的场景,再不改变原来的代码的前提下完成我们的开发诉求。具体场景可以是一个日志中间件,支持跨域中间件。

洋葱模型

io.jpeg

从模型我们也可以看出两个特点

  • 第一每一个中间件都可以对请求响应进行修改,并且往下传递。
  • 第二控制权不断的发生着转义,由外而内,再由内而外。

从模型本身来看我们需要一个有序数组,我们还需要控制流程。实现原理还是用了一个简单的递归去控制流程,加上next函数的执行,实现了请求由外入内,再又内到外。

const http = require('http')
const port = 9527
// http服务封装类
class Middleware {
  constructor() {}
  // 中间件集合
  mids = []
  use(midFn) {
    this.mids.push(midFn)
  }
  // 关系存储
  getRouters = new Map()
  // 监听端口方法
  listen(port) {
      this.httpModel = http.createServer((req, res) => {
      let index = 0
      const midHandler = () => {
        const next = () => {
          index++
          if (this.mids[index]) {
            midHandler()
          } else {
            if (req.method === 'GET') {
              if (this.getRouters.get(req.url)) {
                this.getRouters.get(req.url)(req, res)
              } else {
                res.end()
              }
            } else {
            }
          }
        }
        if (this.mids[index]) {
          this.mids[index](req, res, next)
        } else {
          if (req.method === 'GET') {
            if (this.getRouters.get(req.url)) {
              this.getRouters.get(req.url)(req, res)
            } else {
              res.end()
            }
          } else {
          }
        }
      }
      midHandler()
    })
    this.httpModel.listen(port)
  },
  // get请求map关系建立
  get(url, callback) {
    this.getRouters.set(url, callback)
  }
}

// 实例化对象
const app = new Middleware()

// 日志中间件
const logger = (req, res, next) => {
  console.log('开始记录日志')
  const start = Date.now()
  next()
  const ms = Date.now() - start
  console.log(req.method, req.url, res.statusCode, `处理耗时${ms}ms`)
}

// 支持跨域中间件
const cors = (req, res, next) => {
  console.log('支持了跨域')
  res.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Content-Type': 'application/json;charset=utf-8',
  })
  next()
}

//添加中间件
app.use(logger)
app.use(cors)
// 拦截请求
app.get('/api/getUser', (req, res) => {
  res.end(JSON.stringify({ name: 'wuwenzhou' }))
})
// 启动监听
app.listen(port)

todo

这仅仅是一个最简单的实现,帮助大家了解一下一个node服务的创建,路由的实现,中间件中控制,核心的很多场景都是没有实现的例如异步的支持,安全的校验,执行的去重,异常的处理等等。正在开发中大家还是要细化场景,完善整个逻辑。