写在前面
在vue项目中使用proxy代理,需要定期更换代理的token值,否则登录权限会失效。可考虑对代理的内容进行热更新,修改token值时,也能立即变更代理。 我们所需要的特征如下:
- 代理的配置项,提取在独立配置文件中,监控文件变化并及时响应;
- 响应配置项变化后,重新调整代理服务。
一、文件监控 chokidar
为满足特征1,我们可以将 easy-proxy 的配置项提取为一个 proxy.json 作为配置文件,单独控制配置项。还需要一个文件监控工具,能在 proxy.json 中的 cookie 内容变更后,及时响应。
chokidar ,轻便且高效的跨平台Watch库。调用实例化对象的 watch 方法传入文件路径,即可创建一个 watcher,随后添加事件监听器。
const chokidar = require('chokidar');
chokidar.watch('./proxy.json').on('all', (event, path) => {
console.log(event, path);
// add
// change
});
在事件监听器中,可以使用 Node 的 fs 模块取到文件内容,并解析处理。
改造 devServer 如下:
const fs = require("fs")
const path = require("path")
const chokidar = require("chokidar")
module.exports = {
// ...
devServer: {
// 在服务内部的所有其他中间件之前, 提供执行自定义中间件的功能。
// 这可以用来配置自定义处理程序
before(app) {
let file = './proxy.js'
chokidar.watch(file).on('all', (event, path) => {
const content = readFile(file) // 提取文件内容
const proxy = getProxyData(content, file) // 处理内容数据
// 代理模块...
}
}
},
// ...
}
// 提取文件内容
function readFile(file) {
const data = fs.readFileSync(path.join(__dirname, file), 'utf-8')
return data || readFile(file)
}
// 处理内容数据
function getProxyData(proxyStr, file) {
try {
if(file.endsWith('.js')){
func = new Function(`${proxyStr}; return proxy`)
} else if(file.endsWith('.json')) {
func = new Function(`return ${proxyStr}`)
}
const proxy = func();
return proxy;
} catch {
return {};
}
}
二、服务代理
此处我们选用 http-proxy-middleware 作为代理中间件。依据文档中提示,我们需要保证数据最后呈现结果如下:
const createProxyMiddleware = require("http-proxy-middleware")
app.use(
'/api',
createProxyMiddleware({
target: 'http://www.example.org/api',
changeOrigin: true,
pathRewrite: {
'/api': ''
},
on: {
proxyReq: (proxyReq, req, res) => {
/* handle proxyReq */
},
},
})
);
注意
http-proxy-middleware 在 ^1.0 以上的版本,取 createProxyMiddleware 时需要进行解构
const { createProxyMiddleware } = require("http-proxy-middleware")
原始数据为 json 时如下:
{
"/api": {
"target": "http://10.1.2.345",
"changeOrigin": true,
"cookie": "JSESSIONID=05D9231969DA0F589B241105C4588AA6",
"pathRewrite": {
"/api": ""
}
},
"/acms/ui": {
"target": "http://10.1.2.345",
"changeOrigin": true
}
}
针对由此取到的 proxy 数据,可作如下转换操作:
let realProxy = JSON.parse(JSON.stringify(proxy))
for (const pathContext in proxy) {
initProObj(realProxy[pathContext])
app.use(pathContext, createProxyMiddleware(realProxy[pathContext]))
}
// 装饰代理数据
function initProObj (proObj) {
proObj.getCookie = () => {
return proObj.cookie || ''
}
proObj.getTarget = () => {
return proObj.target
}
proObj.getHost = () => {
const target = proObj.target || ''
return target.split("//")[1] || ''
}
proObj.onProxyReq = (proxyReq) => {
proxyReq.setHeader('Cookie', proObj.getCookie())
proxyReq.setHeader('Referer', proObj.getTarget())
proxyReq.setHeader('Host', proObj.getHost())
}
proObj.router = () => proObj.getTarget()
}
随后,进行封装处理,在 chokidar 中的 change 事件中监听到数据的变动,反映到 httpProxyMiddleware 中,与此同时,chokidar 的 add 事件也需监听进行初始化。
devServer: {
before(app) {
let realProxy = {}
let file = './proxy.js'
chokidar.watch(file).on('all', (event, path) => {
const content = readFile(file)
const proxy = getProxyData(content, file)
if (event === 'add') {
realProxy = JSON.parse(JSON.stringify(proxy))
for (const pathContext in proxy) {
initProObj(realProxy[pathContext])
app.use(pathContext, createProxyMiddleware(realProxy[pathContext]))
}
}
if (event === 'change') {
for (const pathContext in proxy) {
if (realProxy[pathContext]) {
realProxy[pathContext].cookie = proxy[pathContext].cookie
realProxy[pathContext].target = proxy[pathContext].target
} else {
realProxy[pathContext] = JSON.parse(JSON.stringify(proxy[pathContext]))
initProObj(realProxy[pathContext])
app.use(pathContext, createProxyMiddleware(realProxy[pathContext]))
}
}
}
})
},
}
至此达成特征2,提取为工具模块,即可完成全部内容。 由于配置文件为 json 格式,不便注释,可替换为 js 类型,仅需调整解析方法即可。
三、业务中使用
在 vue.config.js 中,调整 devServer 为
const proxyWatch = require('./proxyConfig')
module.exports = {
// ...
devServer: {
before(app) {
const FILE_PATH = './proxy.js'
proxyWatch(app, FILE_PATH)
},
},
// ...
}
proxyConfig.js 文件如下
const fs = require("fs")
const path = require("path")
const chokidar = require("chokidar")
const createProxyMiddleware = require("http-proxy-middleware")
// 读取文件内容
function readFile(file) {
const data = fs.readFileSync(path.join(__dirname, file), 'utf-8')
return data || readFile(file)
}
// 解析文件内容
function getProxyData(proxyStr, file) {
try {
let func = ''
if(file.endsWith('.js')){
func = new Function(`${proxyStr}; return proxy`)
} else if(file.endsWith('.json')) {
func = new Function(`return ${proxyStr}`)
}
const proxy = func();
return proxy;
} catch {
return {};
}
}
// 装饰代理数据
function initProObj (proObj) {
proObj.getCookie = () => {
return proObj.cookie || ''
}
proObj.getTarget = () => {
return proObj.target
}
proObj.getHost = () => {
const target = proObj.target || ''
return target.split("//")[1] || ''
}
proObj.onProxyReq = (proxyReq) => {
proxyReq.setHeader('Cookie', proObj.getCookie())
proxyReq.setHeader('Referer', proObj.getTarget())
proxyReq.setHeader('Host', proObj.getHost())
}
proObj.router = () => proObj.getTarget()
}
/**
*
* @param {*} app 应用实例
* @param {*} filePath 配置文件相对路径
*/
module.exports = function (app, filePath) {
let realProxy = {}
chokidar.watch(filePath).on("all", (event, path) => {
const content = readFile(filePath);
const proxy = getProxyData(content, filePath);
if (event === "add") {
realProxy = JSON.parse(JSON.stringify(proxy))
for (const pathContext in proxy) {
initProObj(realProxy[pathContext])
app.use(pathContext, createProxyMiddleware(realProxy[pathContext]))
}
}
if (event === "change") {
for (const pathContext in proxy) {
if (realProxy[pathContext]) {
realProxy[pathContext].cookie = proxy[pathContext].cookie
realProxy[pathContext].target = proxy[pathContext].target
} else {
realProxy[pathContext] = JSON.parse(JSON.stringify(proxy[pathContext]))
initProObj(realProxy[pathContext])
app.use(pathContext, createProxyMiddleware(realProxy[pathContext]))
}
}
}
});
};