请求代理上下文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