14.koa中间件功能扩展
Opened this issue · 0 comments
Zijue commented
如何实现一个请求参数解析中间件
Koa
中有个包就是专门做这个的,叫koa-bodyparser
。它可以将我们请求携带的参数进行解析,接下来我们自己实现其中一些功能。
const querystring = require('querystring');
function bodyParser() {
return async (ctx, next) => {
ctx.request.body = await new Promise((resolve, reject) => {
// 接受请求的信息存入数组,待接受完毕之后运行处理逻辑
let bufferArr = [];
ctx.req.on('data', function (chunk) {
bufferArr.push(chunk);
});
ctx.req.on('end', function () {
let type = ctx.get('content-type');
let body = Buffer.concat(bufferArr);
if (type.startsWith('application/x-www-form-urlencoded')) { // 表单格式
resolve(querystring.parse(body.toString()));
} else if (type.startsWith('application/json')) { // json格式
resolve(JSON.parse(body.toString()));
} else if (type.startsWith('text/plain')) { // 纯文本格式,一般需要用户自行判断如何处理该数据
resolve(body.toString());
} else if (type.startsWith('multipart/form-data')) {
// todo
} else {
resolve({});
}
})
});
await next(); // 解析请求体之后,继续向下执行
}
}
module.exports = bodyParser;
上述代码中,还没有处理form-data
格式用于接受文件。我们先试试打印未处理请求的效果是怎么样的:
从图上可以看到请求内容被boundary
分隔符分成了好几部分。我们要解析出对应的内容,就需要对接收的请求体二进制数据做切割。但是Buffer
中没有split
方法,我们可以使用8.Buffer 的应用中实现的split
方法。最终完成如下:
const path = require('path');
const fs = require('fs').promises;
const querystring = require('querystring');
const uuid = require('uuid');
Buffer.prototype.split = function (sep) {
let arr = [];
let offset = 0; // 偏移位置
let current = 0; // 当前找到的索引
let len = Buffer.from(sep).length; // 分隔符真实的长度,单位字节
while (-1 != (current = this.indexOf(sep, offset))) { // 查找到位置(字节)的索引,只要有继续
arr.push(this.slice(offset, current));
offset = current + len;
}
arr.push(this.slice(offset));
return arr;
}
function bodyParser({ dir } = {}) {
return async (ctx, next) => {
ctx.request.body = await new Promise((resolve, reject) => {
// 接受请求的信息存入数组,待接受完毕之后运行处理逻辑
let bufferArr = [];
ctx.req.on('data', function (chunk) {
bufferArr.push(chunk);
});
ctx.req.on('end', function () {
let type = ctx.get('content-type');
let body = Buffer.concat(bufferArr);
if (type.startsWith('application/x-www-form-urlencoded')) { // 表单格式
resolve(querystring.parse(body.toString()));
} else if (type.startsWith('application/json')) { // json格式
resolve(JSON.parse(body.toString()));
} else if (type.startsWith('text/plain')) { // 纯文本格式,一般需要用户自行判断如何处理该数据
resolve(body.toString());
} else if (type.startsWith('multipart/form-data')) { // form-data
let boundary = '--' + type.split('=')[1]; // content-type中的分隔符比实际传递的少两个'-'
let lines = body.split(boundary).slice(1, -1); // 切割之后的数组需要去除头尾
let formData = {};
lines.forEach(async function (line) {
/**
Content-Disposition: form-data; name="name"\r\n
\r\n
zijue
*/
let [head, body] = line.split('\r\n\r\n'); // 规范中定义的key、value之间的填充
head = head.toString();
let key = head.match(/name="(.+?)"/)[1];
if (head.includes('filename')) { // 传递的是文件,需要将文件存储到服务器上
/*
Content-Disposition: form-data; name="upload"; filename="test.txt"
Content-Type: text/plain // 此处结尾有不可见字符 \r\n,共计两个字节
// 此处结尾有不可见字符 \r\n,共计两个字节
Zijue
520
Xiaodai
// 此处结尾有不可见字符 \r\n,共计两个字节
*/
// 所以文件内容的区间就为[head部分长度+4, 行总长-2]
let fileContent = line.slice(head.length + 4, -2);
dir = dir || path.join(__dirname, 'upload');
let filePath = uuid.v4(); // 生成唯一文件名
let uploadPath = path.join(dir, filePath);
formData[key] = {
filename: uploadPath,
size: fileContent.length
}
await fs.writeFile(uploadPath, fileContent);
} else {
let value = body.toString();
formData[key] = value.slice(0, -2); // 去除结尾处的 \r\n
}
});
resolve(formData);
} else {
resolve({});
}
})
});
await next(); // 解析请求体之后,继续向下执行
}
}
module.exports = bodyParser;
如何实现koa的静态文件服务器功能
同样的Koa
中也有一个专门处理该问题的包,叫koa-static
。当用户访问指定静态文件目录时,返回该文件的内容。实现起来很简单,代码如下:
const path = require('path')
const fs = require('fs').promises
async function static(staticPath) {
return async (ctx, next) => {
try {
let filePath = path.join(staticPath, ctx.path);
let statObj = await fs.stat(filePath);
if (statObj.isDirectory()) { // 如果是文件夹 会查找index.html
filePath = path.join(filePath, 'index.html')
}
ctx.body = await fs.readFile(filePath, 'utf-8');
} catch (e) { // 报错说明处理不了 没有这个文件
await next(); // 继续向下执行
}
}
}
module.exports = static;
简单实现Koa路由router的功能
Koa
中有一个包@koa/router
就是专门处理路由映射问题的。实现原理如下:
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) {
let dispatch = (i) => {
if (i == layers.length) return next();
let callback = layers[i].callback;
return Promise.resolve(callback(ctx, () => dispatch(i + 1)))
}
return dispatch(0);
}
routes() {
return async (ctx, next) => { // app.routes()的返回结果,标准的中间件函数
let path = ctx.path;
let method = ctx.method;
// 当请求来临时,从暂存的栈中过滤出与之相匹配的路径,可能有多个
let layers = this.stack.filter(layer => layer.match(path, method));
this.compose(layers, ctx, next);
}
}
};
['get', 'post'].forEach(method => [
Router.prototype[method] = function (path, callback) {
let layer = new Layer(path, method, callback);
this.stack.push(layer);
}
])
module.exports = Router
路由的使用方法:
router = new Router();
router.get('/login', async (ctx, next) => {
console.log('login-1');
await next();
})
router.get('/login', async (ctx, next) => {
console.log('login-2')
await next()
})
router.post('/login', async (ctx, next) => {
console.log('login-post')
await next();
})
app.use(router.routes());
当使用不同请求方式时,会走到不同的处理中间件中。
如何优雅地在koa上扩展业务功能代码
module.exports = function (app) {
app.use(async (ctx, next) => {
if (ctx.path === '/login' && ctx.method == 'POST') {
// 验证用户密码,生成cookie之类的
console.log('todo');
} else {
await next();
}
});
}
// 然后在主逻辑代码中引入使用
const login = require('./login');
login(app); // 类似于装饰器的方式
上述代码具体地址koa中间件的使用