egg中开发http-proxy中间件代理转发请求

需求背景:项目中有需要转发的接口,如果普通使用node做转发会存在很多额外的转发逻辑代码,而且这些代码都是重复的,需要做一层中间件代理转发去处理这些重复逻辑。

涉及技术:egg框架、http-proxy库
安装:

npm install http-proxy --save

我们首先搭建一个普通的中间件:
middleware 文件夹中定义中间件文件,如 proxy.js

module.exports = (option) => {
    return async function proxy(ctx, next) {
        // 获取配置所传的参数
        console.log(option);
        // 实现中间件的功能
        await next();
    }
}

路由:

const proxy = app.middleware.proxy; // 代理
router.get('/api/xx', proxy());

在proxy文件中,引入http-proxy

const httpProxy = require('http-proxy');

按照官方文档编写:

try {
           let targetConfig = {target: 'http://...',}//一些配置
           //创建一个代理服务
           const proxy = httpProxy.createProxyServer(
               Object.assign({
                   changeOrigin: true,
                   ignorePath: true,
                   secure: false,
                   logLevel: 'debug'
               }, targetConfig)
           );

           //监听代理服务错误
           proxy.on('error', function (err) {
               console.log('监听代理服务错误',err);
           });

          proxy.web(ctx.req, ctx.res, err => {
                  
          })
       } catch (error) {
           console.log('错误', error)
           ctx.body = {
               code: 403,
               data: '',
               msg: 'http-proxy代理错误'
           };

       }

到这里当时以为大功告成,没什么难度,但请求的时候一直报204,想了很久也看了不少博文,后来跑去翻了大佬封装的http-proxy-middleware和egg-http-proxy源码作对比找差别,发现和http-proxy-middleware的方法差不多,只是没封装一些配置,但在egg-http-proxy发现在请求代理用了

const c2k = require('koa2-connect');
 c2k(proxy(context, proxyOptions))(ctx, next);// 这里的proxy相当于上面中间件的返回async function proxy(ctx, next) {}

egg-http-proxy调用c2k这个插件来包装了一层,所以我又去返回c2k 的源码,这个源码就比较简单了,只有三个方法:

  • koaConnect: 对外公布的方法, 对express的中间件的参数进行分析,分别调用noCallbackHandler和withCallbackHandler
  • noCallbackHandler : 处理无回调的express的中间件
  • withCallbackHandler : 处理有回调的express的中间件

核心其实是noCallbackHandler和withCallbackHandler两个方法

/**
 * If the middleware function does declare receiving the `next` callback
 * assume that it's synchronous and invoke `next` ourselves
 */
function noCallbackHandler(ctx, connectMiddleware, next) {
  connectMiddleware(ctx.req, ctx.res)
  return next()
}

/**
 * The middleware function does include the `next` callback so only resolve
 * the Promise when it's called. If it's never called, the middleware stack
 * completion will stall
 */
function withCallbackHandler(ctx, connectMiddleware, next) {
  return new Promise((resolve, reject) => {
    connectMiddleware(ctx.req, ctx.res, err => {
      if (err) reject(err)
      else resolve(next())
    })
  })
}

/**
 * Returns a Koa middleware function that varies its async logic based on if the
 * given middleware function declares at least 3 parameters, i.e. includes
 * the `next` callback function
 */
function koaConnect(connectMiddleware) {
  const handler = connectMiddleware.length < 3
    ? noCallbackHandler
    : withCallbackHandler
  return function koaConnect(ctx, next) {
    return handler(ctx, connectMiddleware, next)
  }
}

module.exports = koaConnect

所以在自己写的中间件中加入了withCallbackHandler 的方法

try {
            let targetConfig = {target: 'http://...',}//一些配置
            //创建一个代理服务
            const proxy = httpProxy.createProxyServer(
                Object.assign({
                    changeOrigin: true,
                    ignorePath: true,
                    secure: false,
                    logLevel: 'debug'
                }, targetConfig)
            );

            //监听代理服务错误
            proxy.on('error', function (err) {
                console.log('监听代理服务错误',err);
            });

           return new Promise((resolve, reject) => {
                proxy.web(ctx.req, ctx.res, err => {
                    if (err) reject(err)
                    else resolve(next())
                })
            })
        } catch (error) {
            console.log('错误', error)
            ctx.body = {
                code: 403,
                data: '',
                msg: 'http-proxy代理错误'
            };

        }

这样就正常返回了,之前一直报204是因为缺了一层返回,导致一直都没有正常的返回体。

另外还封装了一下路径重写和配置

实际用起来发现除了get请求,其他post,delete请求都不行,
原因是express框架封装了一下请求的body格式,这里我使用的egg也是一样的道理,需要处理一下
req.body或者ctx.request.rawBody看情况选择,egg选择ctx.request.rawBody

// 处理body参数
            proxy.on('proxyReq', function (proxyReq, req, res, options) {
                // console.log('代理',ctx.request.body)
                if (ctx.request.rawBody) {
                //   let bodyData = JSON.stringify(ctx.request.rawBody)
                  let bodyData = ctx.request.rawBody
                  // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
                //   proxyReq.setHeader('Content-Type', 'application/x-www-form-urlencoded')
                  proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData))
                  // stream the content
                  proxyReq.write(bodyData)
                }
            })

完整代码:

const httpProxy = require('http-proxy');
import * as _ from 'lodash';
export default (options={})=> {
     /**
     * defaultOpt通用配置
     * options特殊配置,其中defaultOpt对应proxyTabel的默认配置
     */
    return async function proxy(ctx, next) {
        // console.log(app.config.proxyTabel)
        let targetConfig:any = {}

        // 获取配置
        // 通用配置
        let defaultOpt = {}
        let proxyConfig = _parsePathRewriteRules(ctx.app.config.proxyTabel)
         if (options.defaultOpt) {
            defaultOpt = ctx.app.config.proxyTabel[options.defaultOpt]
          } else {
            let arr = proxyConfig.filter((item=>{
                return ctx.request.url.match(item.regex)
            }))
            defaultOpt = arr[0].value
          }

        // 结合特殊配置
        if (JSON.stringify(options)=="{}") {
            targetConfig = JSON.parse(JSON.stringify(defaultOpt))
        } else {
            let obj = Object.assign({}, defaultOpt, options)
            targetConfig = JSON.parse(JSON.stringify(obj))
        }
        // 重写路由
        let path = _parsePathRewriteRules(targetConfig.pathRewrite)
        let query = ctx.request.url
        _.map(path, (item=>{
            query = query.replace(item.regex,item.value)
        }))
        targetConfig.target = targetConfig.target + query
        console.log('代理地址:', targetConfig.target)

        try {
            //创建一个代理服务
            const proxy = httpProxy.createProxyServer(
                Object.assign({
                    changeOrigin: true,
                    ignorePath: true,
                    secure: false,
                    logLevel: 'debug'
                }, targetConfig)
            );

            //监听代理服务错误
            proxy.on('error', function (err) {
                console.log('监听代理服务错误',err);
            });

            // 处理body参数
            proxy.on('proxyReq', function (proxyReq, req, res, options) {
                // console.log('代理',ctx.request.body)
                if (ctx.request.rawBody) {
                //   let bodyData = JSON.stringify(ctx.request.rawBody)
                  let bodyData = ctx.request.rawBody
                  // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
                //   proxyReq.setHeader('Content-Type', 'application/x-www-form-urlencoded')
                  proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData))
                  // stream the content
                  proxyReq.write(bodyData)
                }
            })

            return new Promise((resolve, reject) => {
                proxy.web(ctx.req, ctx.res, err => {
                    if (err) {
                        reject(err)
                    } else {
                        resolve(next())
                    }
                })
            })
        } catch (error) {
            console.log('错误', error)
            ctx.body = {
                code: 403,
                data: '',
                msg: 'http-proxy代理错误'
            };

        }
    }
}

// 转换对象正则为数组
function _parsePathRewriteRules(rewriteConfig) {
    const rules: any = []
  
    if (_.isPlainObject(rewriteConfig)) {
        _.forIn(rewriteConfig, (value, key) => {
            let obj = {
                regex: new RegExp(key),
                value: rewriteConfig[key],
            }
            rules.push(obj);
        // logger.info('[HPM] Proxy rewrite rule created: "%s" ~> "%s"', key, rewriteConfig[key]);
        });
    }

    return rules;
}

路由router.ts:

const proxy = app.middleware.proxy; // 代理
router.get('/api/。。。', proxy({defaultOpt:'TEST'}));
// 或者
router.get('/api/。。。', app.middleware.proxy({pathRewrite: {'^/api/..': '/..'}}));

通用配置config.default.ts:

config.proxyTabel = { // 按照http-proxy的配置参数,另外加上pathRewrite
        'TEST':{ // 对应defaultOpt
            target: 'http://...',
            pathRewrite: { 
                ....
            },
        }
        '^/api/....':{
            target: 'http://...',
            pathRewrite: { 
                ....
            },
            headers: {
                ....
            },
            // changeOrigin: true,
        },
    };

完毕。

你可能感兴趣的:(egg中开发http-proxy中间件代理转发请求)