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-middlewareegg-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,
        },
    };

完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354