前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Koa 中间件实现

Koa 中间件实现

原创
作者头像
测不准
发布2021-07-31 17:57:06
5501
发布2021-07-31 17:57:06
举报
文章被收录于专栏:与前端沾边与前端沾边

前面我们介绍过了,Koa 的核心就是中间件机制,起服务的话都是千篇一律的。中间件从上至下决定了执行顺序,我们可以在路由之前做权限认证等自己的操作,本篇分享下 koa 几个中间件的实现,也就是把 use 的回调函数单独提出去重写,由于我们会传递参数,所以不会直接返回一个函数,而是一个高阶函数

static 中间件

这个函数作用是读取本地的静态文件,提供一下目录,服务启动就可以直接在浏览器访问内部文件了,主要是用了 fs 模块

代码语言:txt
复制
const path = require('path') // 路径处理

// fs 方法可以直接使用 promise 调用,我们下次分享下 node 内部链式函数的实现
// 对大家有帮助的话,可以点下赞、关注下哈
const fs = require('fs').promises

// 高阶函数返回函数(闭包), 导出的函数可以传参。如果直接导出 async(ctx, next) => {}  没有拓展操作了
module.exports = function static(staticPath) {
  return async (ctx, next) => {
    // 输入的路径可能不存在
    try {
      // 拼接,路径里面可能有 / ,node 会默认根路径,这里使用 join
      let filePath = path.join(staticPath, ctx.path)
      // 这里不要使用 fs.exists 判断文件了, 使用 stat 或者 lstat
      let statObj = awiat fs.stat(filePath)
      if (statObj.isDirection()) {
        // 文件夹默认访问内部的 index.html
        filePath = path.join(filePath, 'index.html')
      }
      可以用流的形式,这里就直接读取了
      ctx.body = await fs.readFile(filePath, 'utf-8')
    } catch(error) {
      return await next()
    }
  }
}
  • 使用
代码语言:txt
复制
app.use(static(__dirname))

http://localhost:3000/form.html

koa-rotuer 中间件

我们先看下官方的使用

代码语言:txt
复制
const Router = require('koa-router')
// 导出的是一个类
let router = new Router()

// 有 get  post 等等方法
router.get('/login', async (ctx, next) => {
  await next()
})

// 一个高阶函数
app.use(router.routes())

实现下我们的 demo

代码语言:txt
复制
// 基础架子
class Router {
  constructor() {}
  get(path) {}
  routes() {
    return async (ctx, next) => {
    
    }
  }
}

我们使用路由一个方法时可以多次调用

代码语言:txt
复制
router.get('/', fn)
router.get('/', fn2)
router.get('/', fn3)
router.post('/', fn1)

所以我们想到使用栈的形式存储,执行的时候依次遍历,我们想到洋葱模型的执行机制,最后统一调用。那么存到栈中的就是个对象
{
  path: '',// 遍历执行要匹配
  callback: , // 匹配到了要依次执行
  method: , // 同一个路径可能不同的方法
}

这里我们使用类的形式

改写

代码语言:txt
复制
// 定义一个存储对象的格式

// 单纯的用 对象也可以
class Layer {
  constructor(path, method, callback) {
    this.path = path
    this.method = method
    this.callback = callback
  }
  // 从栈中筛选出当前匹配。 方法调用绑定在自己身上,避免属性暴露外面对比,拓展性不好,暴露一个方法传参即可,有特殊情况直接在 该方法内部添加即可
  match(path, method) {
    return this.path == path && this.method == method.toLowerCase()
  }
}

class Router {
  constructor() {
    // 存储所有的路由,执行的时候过滤
    this.stack = []
  }
  
  compose(layers, ctx, next) {
    const dispatch = i => {
      if (i === layers.length) reutrn next()
      let cb = layers[i].callback
      return Promise.resolve(cb(ctx, () => dispatch(i + 1)))
    }
    return dispatch(0)
  }
  
  // 在入口中 调用
  routes() {
    // 真实的中间件执行
    return async (ctx, next) => {
      let path = ctx.path
      let method = ctx.method
      // 做筛选
      let layers = this.stack.filter(layer => {
        return layer.match(path, method)
      })
      
      // 筛选出要执行的 layer 了,仿照中间件执行模式,依次执行
      this.compose(layers, ctx, next)
    }
  }
}

// 这里可以下载 methods 库,里面包含了 所有方法名,不用单独在类中配置方法,因为内部的结构是一样的。直接循环
['get', 'post', ...].forEach(method => {
  Router.prototype[method] = function(path, callback) {
    let layer = new Layer(path, method, callback)
    this.stack.push(layer)
  }
})

body-parser 中间件

get 请求我们都知道从 url 中获取参数,使用 url.parse 解析后,直接在 query 参数中,使用 ctx.query 就能获取。对于 post 这种参数在 body 中的形式,我们需要用流的形式读取 buffer 做拼接,而且对于上传图片的形式,还要了解 mulpart/form-data 这种形式。

提交格式类型

简单介绍下几个常见类型

  • application/x-www-form-urlencoded

传递的表单数据

  • application/json

传递的是普通的 json 数据

  • mulpart/form-data

上传文件

Content-Type: multipart/form-data; boundary=你的自定义boundary

分隔符中间夹着的就是传输的数据,还记得我们的 form 表单提交会有一个 name 属性

代码语言:txt
复制
<input name="username" /> , 匹配找到 username 对应的值转为对象形式

有些头部类型是 application/json;charset=utf-8,分号后面还有个描述,为了判断数据类型方便起见,我们用头部匹配判断 startsWith

接收数据

使用我们上面写的 static 中间件,访问本地的简单的一个页面,写个表单吧,为后面准备,我们先写个 post 请求普通对象形势

script 脚本中直接执行下我们的请求

代码语言:txt
复制
fetch('/json', {
  method: 'post',
  headers: {
    // 默认是 text/plain
    'content-type': 'application/json'
  },
  body: JSON.stringify({a: 456})
}).then(res => res.json()).then(data => {
  console.log(data)
})

服务端接收数据

代码语言:txt
复制
router.post('/json', (ctx, next) => {
  let arr = []
  // 这里使用原生的数据监听,流的形势
  ctx.req.on('data', (chunk) => {
    // 二进制形式
    arr.push(chunk)
  })

  ctx.req.on('end', () => {
    console.log(Buffer.concat(arr).toString())
  })
  ctx.body = '456'
})

接口打印

但是我们的监听流的传递是异步的,当我们返回 ctx.body 时,还没有拿到,所以这里需要改成 await promise 形势,然后我们针对不同的 content-type, 组不同的数据处理,把得到的 body 中的值,绑定到 ctx.req.body 上,这样后面执行的中间件就都能获取到了。

实现 bodyParser 中间件

备注使用都在代码中做了标记,大家可以从上往下看,应该很好理解

代码语言:txt
复制
// dir 如果传文件 存储目录
function bodyParser({ dir } = {}) {
  return async (ctx, next) => {
    // 同步处理返回结果,才好赋值,后面的中间件都可以拿到
    ctx.request.body = await new Promise((resolve, reject) => {  
      // 获取 数据流  on  data获取数据
      let arr = []
      
      ctx.req.on('data', function(chunk){
        arr.push(chunk) // chunk buffer数据
      })
      
      // 数据接收完毕, 尽量不要直接使用 原生的req, res 操作
      ctx.req.on('end', () => {
        // 获取用户传递的数据格式 Content-Type
        let type = ctx.get('content-type') // ctx 中已经做了封装 ,或者 res.headers['content-type']
        
        // 拼装 数据
        let body = Buffer.concat(arr)
        
        // 数据类型判断
        if (type === 'application/x-www-form-urlencoded') {
        // 普通表单数据
        // 设置响应头  数据类型  根据实际情况返回的设置
          ctx.set('Content-Type', 'application/json;charset-utf-8')
          
          // 转成对象返回, querystring node自带的内置库
          resolve(querystring.parse(body.toString()))
        } else if (type.startsWith('text/plain')) {
          // 文本类型
          resolve(body.toString())
        } else if (type.startsWith(application/json)) {
          resolve(JSON.parse(body.toString()))
        } else if (type.startsWith('multipaer/form-data')) {
          // 我们上面介绍了 multipaer 类型的请求数据格式,使用 boundary 分割,分割的每一组的头和体是以 \r\n\r\n 做的分割(http 协议规定的,我们可以直接查找替换) (看下面图我们的页面和node接收到的结果)
          
          let boundary = '--' + type.split('=')[1]
          
          // 获取组数。这里因为我们的 body 是 buffer格式,buffer 没有内置的 split 方法,我们需要自己拓展一下类似 数组的 split 方法,看下面
          let lines = body.split(boundary).slice(1, -1) // 掐头去尾,打印后可以看到,标志的开始和结束没有实际意义
          
          // 定义我们要获取的数据
          let formData = {}
          
          lines.forEach(line => {
            let [head, body] = line.split('\r\n\r\n')// 规定的分割方式
            
            head = head.toString()
            // 我们从下面截图可以看到 格式都是 name=xxx
            let key = head.match(/name="(.+?)"/)[1]
            
            // 文件
            if (head.includes('filename')) {
              // 如果收到 文件,我们存到服务器上
              // 获取文件内容
              let content = line.slice(head.length + 4, -2) // +4 因为 \r\n\r\n 分割的,去掉尾部的 \r\n  -2
              
              // 创建上传目录  目录我们可以在执行的时候上传, 默认 upload目录
              dir = dir || path.join(__dirname, 'upload')
              
              // 随机产生文件名
              let filePath = uuid.v4() //uuid 第三方库 生成随机
              let uploadUrl = path.join(dir, filePath)
              
              fs.writeFileSync(uploadUrl, content)
              formData[key] = {
                filename: uploadUrl,
                size: content.length,
                ..... 还可以自己添加需要的属性
              }
            } else {
              let value = body.toString()
              // 去掉后面的 \r\n
              formData[key] = value.slice(0, -2)
            }
          })
          resolve(formData)
        } else {
          // 默认空对象
          resolve({})
        }
      })
    })
    await next() // 继续执行后面的中间件, 请求体的值已经存储了
  }
}


// 拓展 buffer 的 split 方法
Buffer.prototype.split = function(sep) { // 分隔符
  let arr = []
  let offset = 0
  // 分隔符可能是中文,或者特殊符号,所以我们统一转成 buffer 获取长度
  let len = Buffer.from(sep).length
  // 使用 indexof 获取 sep 的位置,放到数组中,返回数组
  while (-1 !== (index = this.indexOf(sep, offset))) { // indexOf第二个参数标识从哪里开始搜索,不会每次从索引 0 往后遍历
    arr.push(this.slice(offset, index))
    offset = index + len
  }
  // 最后一段可能没有分隔符 剩多少放多少  a|b|c  放 c
  arr.push(offset)
  return arr
}

(打印 body.toString())

本篇分享了三个 koa 中比较常见的中间件,其实中间件的形式都是通用的,高阶函数返回,写的都是简单版本,如果大家感兴趣可以自己看源码详细了解。下次计划跟大家分享下 express 的实现机制,比较复杂,会梳理通顺后再写成文章分享给大家。本文有任何疑问可以评论留言。

如果感兴趣的话可以给波关注哈!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • static 中间件
  • koa-rotuer 中间件
  • body-parser 中间件
    • 提交格式类型
      • 接收数据
        • 实现 bodyParser 中间件
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档