Koa 中间件实现
前面我们介绍过了,Koa
的核心就是中间件机制,起服务的话都是千篇一律的。中间件从上至下决定了执行顺序,我们可以在路由之前做权限认证等自己的操作,本篇分享下 koa
几个中间件的实现,也就是把 use
的回调函数单独提出去重写,由于我们会传递参数,所以不会直接返回一个函数,而是一个高阶函数。
static 中间件
这个函数作用是读取本地的静态文件,提供一下目录,服务启动就可以直接在浏览器访问内部文件了,主要是用了 fs
模块
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()
}
}
}
- 使用
app.use(static(__dirname))
http://localhost:3000/form.html
koa-rotuer 中间件
我们先看下官方的使用
const Router = require('koa-router')
// 导出的是一个类
let router = new Router()
// 有 get post 等等方法
router.get('/login', async (ctx, next) => {
await next()
})
// 一个高阶函数
app.use(router.routes())
实现下我们的 demo
版
// 基础架子
class Router {
constructor() {}
get(path) {}
routes() {
return async (ctx, next) => {
}
}
}
我们使用路由一个方法时可以多次调用
router.get('/', fn)
router.get('/', fn2)
router.get('/', fn3)
router.post('/', fn1)
所以我们想到使用栈的形式存储,执行的时候依次遍历,我们想到洋葱模型的执行机制,最后统一调用。那么存到栈中的就是个对象
{
path: '',// 遍历执行要匹配
callback: , // 匹配到了要依次执行
method: , // 同一个路径可能不同的方法
}
这里我们使用类的形式
改写
// 定义一个存储对象的格式
// 单纯的用 对象也可以
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
属性
<input name="username" /> , 匹配找到 username 对应的值转为对象形式
有些头部类型是
application/json;charset=utf-8
,分号后面还有个描述,为了判断数据类型方便起见,我们用头部匹配判断startsWith
接收数据
使用我们上面写的 static
中间件,访问本地的简单的一个页面,写个表单吧,为后面准备,我们先写个 post
请求普通对象形势
script
脚本中直接执行下我们的请求
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)
})
服务端接收数据
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 中间件
备注使用都在代码中做了标记,大家可以从上往下看,应该很好理解
// 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
的实现机制,比较复杂,会梳理通顺后再写成文章分享给大家。本文有任何疑问可以评论留言。
如果感兴趣的话可以给波关注哈!