webpack中proxy代理热更新token

102 阅读3分钟

写在前面

在vue项目中使用proxy代理,需要定期更换代理的token值,否则登录权限会失效。可考虑对代理的内容进行热更新,修改token值时,也能立即变更代理。 我们所需要的特征如下:

  1. 代理的配置项,提取在独立配置文件中,监控文件变化并及时响应;
  2. 响应配置项变化后,重新调整代理服务。

一、文件监控 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]))
        }
      }
    }
  });
};