需求背景:项目中有需要转发的接口,如果普通使用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,
},
};
完毕。