需求背景:項目中有需要轉(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-middleware和egg-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,
},
};
完畢绒净。