egg中開發(fā)http-proxy中間件代理轉(zhuǎn)發(fā)請求

需求背景:項目中有需要轉(zhuǎn)發(fā)的接口鞍盗,如果普通使用node做轉(zhuǎn)發(fā)會存在很多額外的轉(zhuǎn)發(fā)邏輯代碼金赦,而且這些代碼都是重復的拭宁,需要做一層中間件代理轉(zhuǎn)發(fā)去處理這些重復邏輯洛退。

涉及技術(shù):egg框架、http-proxy庫
安裝:

npm install http-proxy --save

我們首先搭建一個普通的中間件:
middleware 文件夾中定義中間件文件杰标,如 proxy.js

module.exports = (option) => {
    return async function proxy(ctx, next) {
        // 獲取配置所傳的參數(shù)
        console.log(option);
        // 實現(xiàn)中間件的功能
        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://...',}//一些配置
           //創(chuàng)建一個代理服務
           const proxy = httpProxy.createProxyServer(
               Object.assign({
                   changeOrigin: true,
                   ignorePath: true,
                   secure: false,
                   logLevel: 'debug'
               }, targetConfig)
           );

           //監(jiān)聽代理服務錯誤
           proxy.on('error', function (err) {
               console.log('監(jiān)聽代理服務錯誤',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源碼作對比找差別掸犬,發(fā)現(xiàn)和http-proxy-middleware的方法差不多袜漩,只是沒封裝一些配置,但在egg-http-proxy發(fā)現(xiàn)在請求代理用了

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

egg-http-proxy調(diào)用c2k這個插件來包裝了一層湾碎,所以我又去返回c2k 的源碼宙攻,這個源碼就比較簡單了,只有三個方法:

  • koaConnect: 對外公布的方法, 對express的中間件的參數(shù)進行分析,分別調(diào)用noCallbackHandler和withCallbackHandler
  • noCallbackHandler : 處理無回調(diào)的express的中間件
  • withCallbackHandler : 處理有回調(diào)的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://...',}//一些配置
            //創(chuàng)建一個代理服務
            const proxy = httpProxy.createProxyServer(
                Object.assign({
                    changeOrigin: true,
                    ignorePath: true,
                    secure: false,
                    logLevel: 'debug'
                }, targetConfig)
            );

            //監(jiān)聽代理服務錯誤
            proxy.on('error', function (err) {
                console.log('監(jiān)聽代理服務錯誤',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是因為缺了一層返回座掘,導致一直都沒有正常的返回體。

另外還封裝了一下路徑重寫和配置

實際用起來發(fā)現(xiàn)除了get請求柔滔,其他post,delete請求都不行,
原因是express框架封裝了一下請求的body格式溢陪,這里我使用的egg也是一樣的道理,需要處理一下
req.body或者ctx.request.rawBody看情況選擇睛廊,egg選擇ctx.request.rawBody

// 處理body參數(shù)
            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
          }

        // 結(jié)合特殊配置
        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 {
            //創(chuàng)建一個代理服務
            const proxy = httpProxy.createProxyServer(
                Object.assign({
                    changeOrigin: true,
                    ignorePath: true,
                    secure: false,
                    logLevel: 'debug'
                }, targetConfig)
            );

            //監(jiān)聽代理服務錯誤
            proxy.on('error', function (err) {
                console.log('監(jiān)聽代理服務錯誤',err);
            });

            // 處理body參數(shù)
            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代理錯誤'
            };

        }
    }
}

// 轉(zhuǎn)換對象正則為數(shù)組
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的配置參數(shù)裕便,另外加上pathRewrite
        'TEST':{ // 對應defaultOpt
            target: 'http://...',
            pathRewrite: { 
                ....
            },
        }
        '^/api/....':{
            target: 'http://...',
            pathRewrite: { 
                ....
            },
            headers: {
                ....
            },
            // changeOrigin: true,
        },
    };

完畢绒净。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市偿衰,隨后出現(xiàn)的幾起案子挂疆,更是在濱河造成了極大的恐慌,老刑警劉巖下翎,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缤言,死亡現(xiàn)場離奇詭異,居然都是意外死亡视事,警方通過查閱死者的電腦和手機胆萧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來俐东,“玉大人跌穗,你說我怎么就攤上這事÷脖瑁” “怎么了蚌吸?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長砌庄。 經(jīng)常有香客問我羹唠,道長,這世上最難降的妖魔是什么娄昆? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任佩微,我火速辦了婚禮,結(jié)果婚禮上稿黄,老公的妹妹穿的比我還像新娘喊衫。我一直安慰自己,他們只是感情好杆怕,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布族购。 她就那樣靜靜地躺著,像睡著了一般陵珍。 火紅的嫁衣襯著肌膚如雪寝杖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天互纯,我揣著相機與錄音瑟幕,去河邊找鬼。 笑死,一個胖子當著我的面吹牛只盹,可吹牛的內(nèi)容都是我干的辣往。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼殖卑,長吁一口氣:“原來是場噩夢啊……” “哼站削!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起孵稽,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤许起,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后菩鲜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體园细,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年接校,在試婚紗的時候發(fā)現(xiàn)自己被綠了猛频。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡馅笙,死狀恐怖伦乔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情董习,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布爱只,位于F島的核電站皿淋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恬试。R本人自食惡果不足惜窝趣,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望训柴。 院中可真熱鬧哑舒,春花似錦、人聲如沸幻馁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仗嗦。三九已至膘滨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間稀拐,已是汗流浹背火邓。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人铲咨。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓躲胳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纤勒。 傳聞我的和親對象是個殘疾皇子坯苹,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353