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

  1. ## 安装依赖
  2. npm i
  3. ## 执行 demo
  4. npm run start
  5. ## 最后启动chrome浏览器访问
  6. ## http://127.0.0.1:3000

依赖

请求体数据流解析方法

  1. module.exports = readStream;
  2. function readStream(req) {
  3. return new Promise((resolve, reject) => {
  4. try {
  5. streamEventListen(req, (data, err) => {
  6. if (data && !isError(err)) {
  7. resolve(data);
  8. } else {
  9. reject(err);
  10. }
  11. });
  12. } catch (err) {
  13. reject(err);
  14. }
  15. });
  16. }
  17. function isError(err) {
  18. return Object.prototype.toString.call(err).toLowerCase() === '[object error]';
  19. }
  20. function streamEventListen(req, callback) {
  21. let stream = req.req || req;
  22. let chunk = [];
  23. let complete = false;
  24. // attach listeners
  25. stream.on('aborted', onAborted);
  26. stream.on('close', cleanup);
  27. stream.on('data', onData);
  28. stream.on('end', onEnd);
  29. stream.on('error', onEnd);
  30. function onAborted() {
  31. if (complete) {
  32. return;
  33. }
  34. callback(null, new Error('request body parse aborted'));
  35. }
  36. function cleanup() {
  37. stream.removeListener('aborted', onAborted);
  38. stream.removeListener('data', onData);
  39. stream.removeListener('end', onEnd);
  40. stream.removeListener('error', onEnd);
  41. stream.removeListener('close', cleanup);
  42. }
  43. function onData(data) {
  44. if (complete) {
  45. return;
  46. }
  47. if (data) {
  48. chunk.push(data.toString());
  49. }
  50. }
  51. function onEnd(err) {
  52. if (complete) {
  53. return;
  54. }
  55. if (isError(err)) {
  56. callback(null, err);
  57. return;
  58. }
  59. complete = true;
  60. let result = chunk.join('');
  61. chunk = [];
  62. callback(result, null);
  63. }
  64. }

解读

  1. const readStream = require('./lib/read_stream');
  2. let strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
  3. let jsonTypes = [
  4. 'application/json'
  5. ];
  6. let formTypes = [
  7. 'application/x-www-form-urlencoded'
  8. ];
  9. let textTypes = [
  10. 'text/plain'
  11. ];
  12. function parseQueryStr(queryStr) {
  13. let queryData = {};
  14. let queryStrList = queryStr.split('&');
  15. for (let [ index, queryStr ] of queryStrList.entries()) {
  16. let itemList = queryStr.split('=');
  17. queryData[ itemList[0] ] = decodeURIComponent(itemList[1]);
  18. }
  19. return queryData;
  20. }
  21. function bodyParser(opts = {}) {
  22. return async function(ctx, next) {
  23. // 拦截post请求
  24. if (!ctx.request.body && ctx.method === 'POST') {
  25. // 解析请求体中的表单信息
  26. let body = await readStream(ctx.request.req);
  27. let result = body;
  28. if (ctx.request.is(formTypes)) {
  29. result = parseQueryStr(body);
  30. } else if (ctx.request.is(jsonTypes)) {
  31. if (strictJSONReg.test(body)) {
  32. try {
  33. result = JSON.parse(body);
  34. } catch (err) {
  35. ctx.throw(500, err);
  36. }
  37. }
  38. } else if (ctx.request.is(textTypes)) {
  39. result = body;
  40. }
  41. // 将请求体中的信息挂载到山下文的request 属性中
  42. ctx.request.body = result;
  43. }
  44. await next();
  45. };
  46. }
  47. module.exports = bodyParser;

使用

  1. const Koa = require('koa');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const body = require('../index');
  5. const app = new Koa();
  6. app.use(body());
  7. app.use(async(ctx, next) => {
  8. if (ctx.url === '/') {
  9. // 当GET请求时候返回表单页面
  10. let html = fs.readFileSync(path.join(__dirname, './index.html'), 'binary');
  11. ctx.body = html;
  12. } else if (ctx.url === '/post' && ctx.method === 'POST') {
  13. // 当POST请求的时候,解析POST表单里的数据,并显示出来
  14. ctx.body = ctx.request.body;
  15. } else {
  16. // 其他请求显示404
  17. ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
  18. }
  19. await next();
  20. });
  21. app.listen(3000, () => {
  22. console.log('[demo] is starting at port 3000');
  23. });
  1. <html>
  2. <head>
  3. <title>example</title>
  4. </head>
  5. <body>
  6. <div>
  7. <p>form post demo</p>
  8. <form method="POST" action="/post">
  9. <span>data</span>
  10. <textarea name="userName" ></textarea><br/>
  11. <button type="submit">submit</button>
  12. </form>
  13. </div>
  14. </body>
  15. </html>

附录

参考

https://github.com/koajs/bodyparser