koa-bodyparser 实现
请求代理上下文context实现
前言
狭义中间件的上下文代理,除了在实例化 let app = new Koa()
的时候将属性或者方法挂载到app.context
中,供后续中间件使用。另外一种方式是在请求过程中在顶端中间件(一般在第一个中间件)使用,把数据或者方法挂载代理到ctx
供下游中间件获取和使用。
这里 请求代理上下文实现 最代表性是官方提供的koa-bodyparser
中间件,这里基于官方原版用最简单的方式实现koa-bodyparser
最简单功能。
常见请求代理上下文context实现过程
- 请求代理ctx
- 直接app.use()
- 在请求过程中过载方法或者数据到上下文
ctx
- 一般在大部分中间件前加载,供下游中间件获取挂载的数据或方法
实现步骤
- step 01
app.use()
在中间件最顶端 - step 02 拦截post请求
- step 03 等待解析表单信息
- step 04 把表单信息代理到ctx.request.body上
- step 05 下游中间件都可以在ctx.request.body中获取表单数据
实现源码
demo源码
https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-03
## 安装依赖
npm i
## 执行 demo
npm run start
## 最后启动chrome浏览器访问
## http://127.0.0.1:3000
依赖
请求体数据流解析方法
module.exports = readStream;
function readStream(req) {
return new Promise((resolve, reject) => {
try {
streamEventListen(req, (data, err) => {
if (data && !isError(err)) {
resolve(data);
} else {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
}
function isError(err) {
return Object.prototype.toString.call(err).toLowerCase() === '[object error]';
}
function streamEventListen(req, callback) {
let stream = req.req || req;
let chunk = [];
let complete = false;
// attach listeners
stream.on('aborted', onAborted);
stream.on('close', cleanup);
stream.on('data', onData);
stream.on('end', onEnd);
stream.on('error', onEnd);
function onAborted() {
if (complete) {
return;
}
callback(null, new Error('request body parse aborted'));
}
function cleanup() {
stream.removeListener('aborted', onAborted);
stream.removeListener('data', onData);
stream.removeListener('end', onEnd);
stream.removeListener('error', onEnd);
stream.removeListener('close', cleanup);
}
function onData(data) {
if (complete) {
return;
}
if (data) {
chunk.push(data.toString());
}
}
function onEnd(err) {
if (complete) {
return;
}
if (isError(err)) {
callback(null, err);
return;
}
complete = true;
let result = chunk.join('');
chunk = [];
callback(result, null);
}
}
解读
const readStream = require('./lib/read_stream');
let strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
let jsonTypes = [
'application/json'
];
let formTypes = [
'application/x-www-form-urlencoded'
];
let textTypes = [
'text/plain'
];
function parseQueryStr(queryStr) {
let queryData = {};
let queryStrList = queryStr.split('&');
for (let [ index, queryStr ] of queryStrList.entries()) {
let itemList = queryStr.split('=');
queryData[ itemList[0] ] = decodeURIComponent(itemList[1]);
}
return queryData;
}
function bodyParser(opts = {}) {
return async function(ctx, next) {
// 拦截post请求
if (!ctx.request.body && ctx.method === 'POST') {
// 解析请求体中的表单信息
let body = await readStream(ctx.request.req);
let result = body;
if (ctx.request.is(formTypes)) {
result = parseQueryStr(body);
} else if (ctx.request.is(jsonTypes)) {
if (strictJSONReg.test(body)) {
try {
result = JSON.parse(body);
} catch (err) {
ctx.throw(500, err);
}
}
} else if (ctx.request.is(textTypes)) {
result = body;
}
// 将请求体中的信息挂载到山下文的request 属性中
ctx.request.body = result;
}
await next();
};
}
module.exports = bodyParser;
使用
const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const body = require('../index');
const app = new Koa();
app.use(body());
app.use(async(ctx, next) => {
if (ctx.url === '/') {
// 当GET请求时候返回表单页面
let html = fs.readFileSync(path.join(__dirname, './index.html'), 'binary');
ctx.body = html;
} else if (ctx.url === '/post' && ctx.method === 'POST') {
// 当POST请求的时候,解析POST表单里的数据,并显示出来
ctx.body = ctx.request.body;
} else {
// 其他请求显示404
ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
}
await next();
});
app.listen(3000, () => {
console.log('[demo] is starting at port 3000');
});
<html>
<head>
<title>example</title>
</head>
<body>
<div>
<p>form post demo</p>
<form method="POST" action="/post">
<span>data</span>
<textarea name="userName" ></textarea><br/>
<button type="submit">submit</button>
</form>
</div>
</body>
</html>