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

需求背景:項目中有需要轉(zhuǎn)發(fā)的接口,如果普通使用node做轉(zhuǎn)發(fā)會存在很多額外的轉(zhuǎn)發(fā)邏輯代碼,而且這些代碼都是重復(fù)的,需要做一層中間件代理轉(zhuǎn)發(fā)去處理這些重復(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)建一個代理服務(wù)
           const proxy = httpProxy.createProxyServer(
               Object.assign({
                   changeOrigin: true,
                   ignorePath: true,
                   secure: false,
                   logLevel: 'debug'
               }, targetConfig)
           );

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

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

       }

到這里當(dāng)時以為大功告成,沒什么難度,但請求的時候一直報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相當(dāng)于上面中間件的返回async function proxy(ctx, next) {}

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

  • koaConnect: 對外公布的方法, 對express的中間件的參數(shù)進(jìn)行分析,分別調(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)建一個代理服務(wù)
            const proxy = httpProxy.createProxyServer(
                Object.assign({
                    changeOrigin: true,
                    ignorePath: true,
                    secure: false,
                    logLevel: 'debug'
                }, targetConfig)
            );

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

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

實際用起來發(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對應(yīng)proxyTabel的默認(rèn)配置
     */
    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)建一個代理服務(wù)
            const proxy = httpProxy.createProxyServer(
                Object.assign({
                    changeOrigin: true,
                    ignorePath: true,
                    secure: false,
                    logLevel: 'debug'
                }, targetConfig)
            );

            //監(jiān)聽代理服務(wù)錯誤
            proxy.on('error', function (err) {
                console.log('監(jiān)聽代理服務(wù)錯誤',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':{ // 對應(yīng)defaultOpt
            target: 'http://...',
            pathRewrite: { 
                ....
            },
        }
        '^/api/....':{
            target: 'http://...',
            pathRewrite: { 
                ....
            },
            headers: {
                ....
            },
            // changeOrigin: true,
        },
    };

完畢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容