项目中配置代理地址后(如webpack配置devServer.proxy后),每次想要切换远程地址,会有一些不方便的情况
- 需要重启服务(不能接受)
- 必须改动文件,会出现git的文件改动,一不小心就会提交到远端仓库和别人产生文件冲突
- 想要同时测试多个服务器环境不可实现,只能一次性修改一个代理地址然后重启服务
为了提高开发效率和排查问题效率,可按照下方的代码实现在浏览器上提供一个remote参数即可直接动态切换proxy代理地址的功能,使用方式如:
此方案有以下优点
- 开发环境生效,生产环境不生效
- 切换代理地址不需要重新前端服务
- 不产生文件改动
- 可同事测试多个代理地址,很简单,打开多个浏览器标签页传递remote参数为不同的服务器地址即可
远程代理插件
会实现三种版本的插件,webpack、vue/cli、vite;
webpack插件不能应用于vue/cli项目中,因为vue/cli项目的devServer配置不会共享到webpack中,webpack插件的方案是借助http-proxy-middleware的router属性(也就是devServer中的router属性)实现的功能,所以在vue/cli中想要实现插件,需要重新封装vue/cli插件
vite的代理功能由于底层用的是http-proxy,所以也需要重新实现一套功能
Webpack插件
插件实现
const urlHelper = require("url");
// url链接用于远程代理的参数名
const REMOTE_KEY = "remote";
// 使用本地proxy的target
const PROXY_STR = "proxy";
/**
* webpack配置devServer.proxy后,每次想要切换远程地址,会有一些不方便的情况
*
* 需要重启服务(不能接受)
* 必须改动文件,会出现git的文件改动,一不小心就会提交到远端仓库和别人产生文件冲突
* 想要同时测试两个服务器环境就得不同的切换
*
* 为了提高开发效率和排查问题效率,可按照下方的代码实现在浏览器上提供一个remote参数即可直接动态切换proxy代理地址的功能,使用方式如:
*
* example: http://localhost:8090?remote=http://test.com
*
* 实现动态代理设计,登录页面登录的时候如果携带remote参数,
* 那么就会使用remote参数的值作为代理地址
* 1. 登录成功后,会将remote参数的值保存到闭包参数中,
*
* @param {*} options.customTarget 自定义处理远程代理地址,传递一个函数
* @param {*} options.remoteParamName remote参数名
* @param {*} options.cacheRemote 是否缓存代理地址,如果开启缓存,切换页面后如果url没有了remote参数,下次请求的时候该参数还是会生效(这样做的原因是,有些项目业务页面跳转不会携带参数,导致remote会丢失,除非重新传递remote参数重置)
* @returns
*/
class RemoteProxyPlugin {
constructor(options = {}) {
this.customTarget =
options.customTarget ||
function (curTarget, _req) {
return curTarget;
}; // 默认空函数
this.remoteParamName = options.remoteParamName || REMOTE_KEY;
this.remoteUrlMap = {};
this.cacheRemote =
options.cacheRemote !== undefined ? options.cacheRemote : true;
}
apply(compiler) {
if (process.env.NODE_ENV !== "development") {
return;
}
const me = this;
compiler.hooks.environment.tap("RemoteProxyPlugin", () => {
const devServer = compiler.options.devServer;
if (devServer && devServer.proxy) {
Object.keys(devServer.proxy).forEach((key, index) => {
const proxyConfig = devServer.proxy[key];
if (typeof proxyConfig == "string") {
devServer.proxy[key] = {
target: proxyConfig,
};
}
me.remoteUrlMap[key] = me.formatRemoteUrl(
devServer.proxy[key].target
)[0];
me.consoleUrl("初始化", key, me.remoteUrlMap[key]);
// 处理 router 逻辑
const originalRouter = devServer.proxy[key].router;
devServer.proxy[key].router = (req) => {
const originalTarget = originalRouter
? originalRouter(req)
: undefined;
let remoteUrl = me.getRemoteUrl(req)
if (remoteUrl) {
remoteUrl = remoteUrl[index] || remoteUrl[0];
}
let newRemoteUrl =
originalTarget || me.customTarget(remoteUrl, req);
// 以下两种情况才会还原代理地址为原来的target
// 1)如果remoteUrl为空并且没有开启缓存时,
// 2)配置了?remote=proxy,需要使用本地proxy配置的target
if (
(!newRemoteUrl && !me.cacheRemote) ||
newRemoteUrl === PROXY_STR
) {
newRemoteUrl = devServer.proxy[key].target;
}
if (newRemoteUrl && newRemoteUrl !== me.remoteUrlMap[key]) {
me.remoteUrlMap[key] = newRemoteUrl;
me.consoleUrl("切换", key, me.remoteUrlMap[key]);
}
return me.remoteUrlMap[key];
};
});
}
});
}
consoleUrl(operText, key, url) {
const boldBlue = `\x1b[34m`; // ANSI escape codes for bold blue
const reset = `\x1b[0m`; // ANSI escape code to reset styles
console.log(
"🚀 " +
boldBlue +
"[RemoteProxyPlugin] " +
key +
" 远程代理地址已" +
operText +
"为:" +
url +
reset
);
}
getRemoteUrl(req) {
// 解析referer参数,获取remoteUrl
return this.formatRemoteUrl(
this.getParseParam(req.headers.referer, this.remoteParamName)
);
}
formatRemoteUrl(remoteUrl) {
if (!remoteUrl) return remoteUrl
// 以","分割,说明传递的是多个地址
const urlArr = remoteUrl.split(',')
return urlArr.map((item) => (item.endsWith('/') ? item.slice(0, -1) : item))
}
/**
* 获取url的query参数
* @param {*} url
* @param {*} key
* @returns
*/
getParseParam(url, key) {
const queryObject = urlHelper.parse(url, true).query;
return queryObject[key];
}
}
module.exports = RemoteProxyPlugin;
注册插件
在webpack.config.js中注册这个插件
const PROXY_TARGET = "http://test.com";
module.exports = {
devServer: {
port: 8090,
open: true,
proxy: {
'/api': {
changeOrigin: true,
target: PROXY_TARGET,
}
},
},
plugins: [
new RemoteProxyPlugin(), // 动态远程代理
// ... other
],
// ... other
};
vue/cli插件
插件实现
const urlHelper = require("url");
// 是否启用日志(打印代理地址变动信息)
const logEnable = true;
// 使用本地proxy的target
const PROXY_STR = "proxy";
// remote参数名
const remoteParamName = "remote";
// 是否缓存代理地址,如果开启缓存,切换页面后如果url没有了remote参数,下次请求的时候该参数还是会生效(这样做的原因是,有些项目业务页面跳转不会携带参数,导致remote会丢失,除非重新传递remote参数重置)
const cacheRemote = true;
// 自定义处理远程代理地址
const customTarget = function (curTarget, _req) {
return curTarget;
};
/**
* 配置devServer.proxy后,每次想要切换远程地址,会有一些不方便的情况
*
* 需要重启服务(不能接受)
* 必须改动文件,会出现git的文件改动,一不小心就会提交到远端仓库和别人产生文件冲突
* 想要同时测试两个服务器环境就得不同的切换
*
* 为了提高开发效率和排查问题效率,可按照下方的代码实现在浏览器上提供一个remote参数即可直接动态切换proxy代理地址的功能,使用方式如:
*
* example: http://localhost:8090?remote=http://test.com
*
* 实现动态代理设计,登录页面登录的时候如果携带remote参数,
* 那么就会使用remote参数的值作为代理地址
*
* @returns
*/
function RemoteProxyPlugin(api, options) {
if (process.argv[2] !== "serve" || process.env.NODE_ENV !== "development") {
return;
}
const proxy = JSON.parse(JSON.stringify(options.devServer.proxy));
const remoteUrlMap = {};
if (proxy) {
Object.keys(proxy).forEach((key, index) => {
const proxyConfig = proxy[key];
if (typeof proxyConfig == "string") {
proxy[key] = {
target: proxyConfig,
};
}
remoteUrlMap[key] = formatRemoteUrl(proxy[key].target)[0];
consoleUrl("初始化", key, remoteUrlMap[key], true);
// 处理 router 逻辑
const originalRouter = proxy[key].router;
proxy[key].router = (req) => {
const originalTarget = originalRouter ? originalRouter(req) : undefined;
let remoteUrl = getRemoteUrl(req);
if (remoteUrl) {
remoteUrl = remoteUrl[index] || remoteUrl[0];
}
let newRemoteUrl = originalTarget || customTarget(remoteUrl, req);
// 如果remoteUrl为空时,如果没有开启缓存,才会还原代理地址为原来的target
if (!newRemoteUrl && !cacheRemote || newRemoteUrl === PROXY_STR) {
newRemoteUrl = proxy[key].target;
}
if (newRemoteUrl && newRemoteUrl !== remoteUrlMap[key]) {
remoteUrlMap[key] = newRemoteUrl;
consoleUrl("切换", key, remoteUrlMap[key]);
}
return remoteUrlMap[key];
};
});
}
options.devServer.proxy = proxy;
}
function consoleUrl(operText, key, url, forceLog = false) {
if (!logEnable && !forceLog) {
return;
}
const boldBlue = `\x1b[34m`; // ANSI escape codes for bold blue
const reset = `\x1b[0m`; // ANSI escape code to reset styles
console.log(
"🚀 " +
boldBlue +
"[RemoteProxyPlugin] " +
key +
" 远程代理地址已" +
operText +
"为:" +
url +
reset
);
}
function getRemoteUrl(req) {
// 解析referer参数,获取remoteUrl
return formatRemoteUrl(getParseParam(req.headers.referer, remoteParamName));
}
function formatRemoteUrl(remoteUrl) {
if (!remoteUrl) return remoteUrl;
// 以","分割,说明传递的是多个地址
const urlArr = remoteUrl.split(",");
return urlArr.map((item) => (item.endsWith("/") ? item.slice(0, -1) : item));
}
/**
* 获取url的query参数
* @param {*} url
* @param {*} key
* @returns
*/
function getParseParam(url, key) {
const queryObject = urlHelper.parse(url, true).query;
return queryObject[key];
}
module.exports = RemoteProxyPlugin;
注册插件
vue/cli的本地插件需要再package.json中引入,在vuePlugins.service中配置本地插件的所在相对路径即可,比如remoteProxyPlugin的相对路径是:scripts/plugins/remoteProxyPlugin.js
{
"name": "xxx",
"version": "1.0.0",
"main": "index.html",
"license": "MIT",
"scripts": {},
"vuePlugins": {
"service": [
"scripts/plugins/remoteProxyPlugin"
]
},
}
vite插件
插件实现
import httpProxy from 'http-proxy'
const REMOTE_KEY = 'remote'
// 使用本地proxy的target
const PROXY_STR = 'proxy'
// 默认是否开启代理缓存
const PROXY_CACHE_REMOTE = true
// 创建代理实例
const proxy = httpProxy.createProxyServer()
/**
* webpack配置devServer.proxy后,每次想要切换远程地址,会有一些不方便的情况
*
* 需要重启服务(不能接受)
* 必须改动文件,会出现git的文件改动,一不小心就会提交到远端仓库和别人产生文件冲突
* 想要同时测试两个服务器环境就得不同的切换
*
* 为了提高开发效率和排查问题效率,可按照下方的代码实现在浏览器上提供一个remote参数即可直接动态切换proxy代理地址的功能,使用方式如:
*
* example: http://localhost:8090?remote=http://test.com
*
* 实现动态代理设计,登录页面登录的时候如果携带remote参数,
* 那么就会使用remote参数的值作为代理地址
* 1. 登录成功后,会将remote参数的值保存到闭包参数中,
*
* @param {*} options.customTarget 自定义处理远程代理地址,传递一个函数
* @param {*} options.remoteParamName remote参数名
* @param {*} options.cacheRemote 是否缓存代理地址,如果开启缓存,切换页面后如果url没有了remote参数,下次请求的时候该参数还是会生效(这样做的原因是,有些项目业务页面跳转不会携带参数,导致remote会丢失,除非重新传递remote参数重置)
* @returns
*/
export default function createRemoteProxyPlugin(options = {}) {
return {
name: 'vite-plugin-remote-proxy',
apply: 'serve',
configureServer(server) {
const proxyCfg = server.config.server.proxy
let remoteUrlMap = new Map()
// 修改 devServer 配置
server.middlewares.use((req, res, next) => {
const proxyKeys = Object.keys(proxyCfg)
let remoteUrl, proxyCfgItem
for (let index = 0; index < proxyKeys.length; index++) {
const context = proxyKeys[index]
let curProxyItem = proxyCfg[context]
const curRemoteUrl = getRemoteUrl(req, remoteUrlMap, context, index, proxyCfg, options)
if (curProxyItem && doesProxyContextMatchUrl(context, req.url)) {
if (typeof curProxyItem === 'string') {
curProxyItem = { target: curProxyItem, changeOrigin: true }
}
remoteUrl = curRemoteUrl
proxyCfgItem = curProxyItem
}
}
if (remoteUrl && proxyCfgItem && proxyWeb({ req, res, remoteUrl, proxyCfgItem })) {
return
}
next()
})
},
}
}
function proxyWeb({ req, res, remoteUrl, proxyCfgItem } = options) {
// 配置了?remote=proxy,不需要特殊代理,直接使用本地proxy配置的target
if (remoteUrl === PROXY_STR) {
return false
}
const proxyOptions = {
...proxyCfgItem,
target: remoteUrl,
}
if (proxyCfgItem.rewrite) {
req.url = proxyCfgItem.rewrite(req.url)
}
// 实现代理
proxy.web(req, res, proxyOptions, (error) => {
console.error('Proxy error:', error)
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('[vite-plugin-remote-proxy] Proxy error occurred')
})
return true
}
function doesProxyContextMatchUrl(context, url) {
return (context[0] === '^' && new RegExp(context).test(url)) || url.startsWith(context)
}
/**
*
* @param {*} req 请求对象
* @param {*} remoteUrlMap 缓存的远程代理地址Map
* @param {*} context 当前匹配的proxy的key
* @param {*} index 当前匹配的proxy的key的索引位置
* @param {*} proxyCfg proxy配置
* @param {*} options 配置的对象
* @returns
*/
function getRemoteUrl(req, remoteUrlMap, context, index, proxyCfg, options = {}) {
const {
customTarget = (curTarget, _req, _proxyCfg) => curTarget,
remoteParamName = REMOTE_KEY,
cacheRemote = PROXY_CACHE_REMOTE,
} = options
const url = req.headers.referer || req.originalUrl || ''
// 解析referer参数,获取remoteUrl
const remoteUrls = formatRemoteUrl(getParseParam(url, remoteParamName))
let remoteUrl
if (remoteUrls) {
remoteUrl = customTarget(remoteUrls[index] || remoteUrls[0], req, proxyCfg)
// 将 remote URL 缓存到内存中
if (remoteUrl && remoteUrl !== remoteUrlMap.get(context)) {
consoleUrl('切换', context, remoteUrl)
remoteUrlMap.set(context, remoteUrl)
}
} else if (cacheRemote && remoteUrlMap.has(context)) {
// 使用缓存的代理地址
remoteUrl = remoteUrlMap.get(context)
}
return remoteUrl
}
function consoleUrl(operText, key, url) {
const boldBlue = `\x1b[34m` // ANSI escape codes for bold blue
const reset = `\x1b[0m` // ANSI escape code to reset styles
console.log(
'🚀 ' + boldBlue + '[vite-plugin-remote-proxy] ' + key + ' 远程代理地址已' + operText + '为:' + url + reset,
)
}
function getParseParam(url, key) {
return new URLSearchParams(url.split('?')[1]).get(key)
}
function formatRemoteUrl(remoteUrl) {
if (!remoteUrl) return remoteUrl
// 以","分割,说明传递的是多个地址
const urlArr = remoteUrl.split(',')
return urlArr.map((item) => (item.endsWith('/') ? item.slice(0, -1) : item))
}
注册插件
const PROXY_TARGET = "http://test.com";
export default defineConfig(({ mode }) => {
const envConfig = loadEnv(mode, process.cwd())
const isDev = envConfig.VITE_ENV === 'development'
return {
server: {
port: 9528,
host: '0.0.0.0',
proxy: {
'/api': {
changeOrigin: true,
target: PROXY_TARGET,
rewrite: (path) => path.replace(/^\/api/, '/'),
}
},
},
plugins: [
createVuePlugin(),
// ... other
],
// ... other
}
})