逃离微前端之:像这些专业系统的功能,客户年前就要在自己的系统里实现怎么办?

最近需要实现一个系统,内含几个子系统,每个子系统有各自的功能、菜单。

比如某个系统叫 信息化设备运维管理 ,在这里实现设备的组网、检测、统计等。

此类功能有一个叫 zabbix 的程序,已经做了很多事情。而这些事情也正是客户所想要的(当然你懂的,也并不全想要),那要从0开发?二开?

不可能!决定不可能!客户年前就要。经协商,我们可以把那些客户想要的功能,集成到我们的系统里面。

所以这件事情可以简单抽象为:需求是在页面内集成三方页面(当前是集成 zabbix 的两个页面),会有自动登录功能。

实现结果

集成前的三方系统:

  • 需要登录
  • 系统里有菜单
  • 有退出功能

逃离微前端之:像这些专业系统的功能,客户年前就要在自己的系统里实现怎么办?

集成后的几个三方系统:

  • 直接进入某页面无需登录
  • 没有菜单等其他无关元素
  • 有 loading 功能

逃离微前端之:像这些专业系统的功能,客户年前就要在自己的系统里实现怎么办?

方案

  • 微前端

力求运行上沙箱完整性,通信上的便利性,附加一些优化,例如预加载、缓存、让弹窗能弹在宿主上。

  • iframe

天然的沙箱,使用符合浏览器实现的安全规则和通信方式,所有元素例如弹窗都只限定在 iframe 中。

  • 自动登录

可以使用单点登录、模拟登录等方式。

微前端

  • 选择

有 qiankun、Micro App、无界等前端框架可以选择。

  • 测试

使用无界和Micro App实现了页面集成,但qiankun总是报错方法没有注入(要求注入一些生命周期到要嵌入的三方系统中)。

  • 结果

对于微前端方案,有以下几点问题:

  1. 兼容性无法保证,例如子系统某些定位元素会跑到父系统上
  2. 复杂,出现兼容性问题到底是哪个环境的问题
  3. 如果要与子系统通信,也需要跨域
  4. 出现问题时某此框架也可以回退到 iframe ,但等发现问题时再想着改到 iframe 可能来不及

iframe

一般只要三方页面没有特别声明不允许通过 iframe 嵌入,都能嵌进去,如果要做通信也需要修改三方系统。但考虑到微前端方案也是一样的要修改,那就选择干扰性较小的 iframe。如果出了问题,无非是代码没加进去或未放开安全声明,不会存在是不是哪个框架的什么方法是不是使用不对,是不是有 bug,到处查框架文档这种问题。

对于增加集成后的使用体验,使用 iframe-resizer 可以让三方页面嵌入之后,自然的随三方页面自适应大小。同时还可以与其进行通信。

详解通过 iframe 实现三方系统登录、通信

因为通信需要修改三方系统,但我又不想动三方系统的代码,所以直接做一层代理。不管是做安全声明,还是代码注入、自动登录,都在这一层实现即可。

安全声明

例如允许三方系统被 iframe 嵌入,我们需要修改响应头 X-Frame-Options,例如值 AllowAll。允许 cookie 写入,由于三方系统是通过 cookie 来实现用户验证的,但嵌入到 iframe 中后,set-cookie 这一操作会被限制,需在 cookie 写入时声明 SameSite 才可以,例如 SameSite=None; Secure

const exampleProxy = createProxyMiddleware({
  target: 'http://192.168.1.191',
  changeOrigin: true,
  selfHandleResponse: true,
  onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
    const cookie = res.getHeader(`set-cookie`)
    res.setHeader('set-cookie', `${cookie}; SameSite=None; Secure `);
    res.setHeader('X-Frame-Options', `AllowAll`);
    return responseBuffer;
  }),
});

代码注入

实际上,通过安全声明,我们就可以实现宿主和三方系统的通信了。但是由于三方系统中并没有我们的代码,所以并不会响应我们的任何操作(例如在宿主中发起登录请求,三方系统即自动登录)。要便于三方系统对我们的操作进行响应,就要在三方系统里注入代码。

例如我们这里可以注入 iframe-resizer 来自适应三方系统的大小,注入 jQuery 来实现 dom 操作,注入编译应用 js 和 css 的函数等。

if ((proxyRes.headers['content-type'] || ``).includes(`text/html`)) {
  const response = responseBuffer.toString('utf8').replace(`<head>`, `
    <head>
    <style>
      // 注入样式
    </style>
    <script>
      // 注入 css 加载器
      function injectCSS(css) {
        const style = document.createElement('style');
        style.innerHTML = css;
        document.head.appendChild(style);
      }
      
      // 注入 js 加载器
      function loadScript(url) {
        return new Promise((resolve, reject) => {
          var script = document.createElement('script');
          script.src = url;
          script.onload = resolve;
          document.head.appendChild(script);
        })
      }
      
      // 注入 iframe-resizer 程序
      ;${iframeResizer}
      ;${contentWindow}

      // 配置 iframe-resizer
      ;window.iFrameResizer = {
        onMessage(js) {
          console.log("js", js)
          eval(js)
        },
        onReady() {
          window.parentIFrame.sendMessage("onReady")
          console.log("onReady")
        }
      }
    </script>
  `).replace(/<aside([\s\S]*?)\/aside>/, '') // 删除菜单
  return response
}

集成到若依框架

在若依这个框架中,菜单配置里,可以配置某个菜单是否为外链,输入三方系统的链接之后,即可嵌入三方系统。

例如我们在这里嵌入 www.hongqiye.com/doc/mockm/ 这个页面:

逃离微前端之:像这些专业系统的功能,客户年前就要在自己的系统里实现怎么办?

看起来是我们想要的样子。

集成自动登录

自动登录一般使用单点登录实现,一般需要三方系统自身有开放此功能。

经过查询,我们要集成的 zabix 是通过 sso 单点登录系统来实现,看起来像以下样子。

逃离微前端之:像这些专业系统的功能,客户年前就要在自己的系统里实现怎么办?

但是我们并没有这个系统,我也不想折腾这个系统,因为原始的 zabix 可以以普通账号密码方式进行表单登录,登录后即可获得授权。

反正我们做了一层代理,那我们就在代理层进行自动登录即可。

const data = await fetch("http://192.168.1.253:9700/index.php", {
  "headers": {
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7",
    "cache-control": "max-age=0",
    "content-type": "application/x-www-form-urlencoded",
    "upgrade-insecure-requests": "1"
  },
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": `request=zabbix.php%3Faction%3Ddashboard.view&name=${name}&password=${password}&autologin=1&enter=%E7%99%BB%E5%BD%95`,
  "method": "POST",
  "mode": "cors",
  "credentials": "include"
});
const sessionid = data.headers.get(`set-cookie`)
res.setHeader(`Set-Cookie`, sessionid)

这样在通过代理访问系统的时候,我们就可以根据某个标识来自动获取 sessionid 。也可以做一个跳转页面来跳转(浏览器 host 与 iframe host 要一致,否则无法 set-cookie ):

app.use(`/auto/:base64`, async (req, res, next) => {
  const html = `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8" />
      <script>
        fetch("/index.php", {
          "headers": {
            "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
            "accept-language": "zh-CN,zh;q=0.9",
            "cache-control": "max-age=0",
            "content-type": "application/x-www-form-urlencoded",
            "upgrade-insecure-requests": "1"
          },
          "referrerPolicy": "strict-origin-when-cross-origin",
          "body": "name=${data.name}&password=${data.password}&autologin=1&enter=%E7%99%BB%E5%BD%95",
          "method": "POST",
          "mode": "cors",
          "credentials": "include"
        }).then(res => {
          window.location.href = '${url}';
        })
      </script>
    </head>
    <body>
      <center><a href="url">${url} 加载中...</a></center>
    </body>
    <style>
        html, body {
          width: 100%;
          height: 100vh;
        }
    </style>
    </html>
  `;
  res.status(200).end(html);
})

我们新建一个路由,这个路由返回一个 html,由 html 中的 js 来请求登录所需的 cookie 并自动跳转。这样即可实现自动登录系统。

传送页面参数

需要注意的是,在若依这个系统中,配置菜单时,菜单路径一样则视为同一个 key ,这个在选择菜单时,相同的 path 不同的菜单,全部都会变为选择状态。

另外,如果填写的 path 过长,那么就会自动在新窗口打开,如果 url 中含有符号点(.),会自动转换为斜杠。这导致我们填写在菜单中的 url 是这样子: http://127.0.0.1/path?action=map.view 那么 iframe 中的 src 收到的实际上 http://127.0.0.1/path?action=map/view ,目前不知道为什么会出现这样的现象,估计是若依内部做了处理,我不想去查改若依的代码。因为在这代理端很容易处理掉:

我们把所有参数都转为一段 base64,这样同时解决了若依的多层 path 问题和参数特殊符号问题。

app.use(`/auto/:base64`, async (req, res, next) => {
  let {base64} = req.params
  let data = JSON.parse(Buffer.from(base64, 'base64').toString('utf-8'))
  const html = `
    // ... to ${data.url}
  `;
  res.status(200).end(html);
})

逃离微前端之:像这些专业系统的功能,客户年前就要在自己的系统里实现怎么办?

若依菜单配置外链的一些特性

这里单独列出来,避免大家踩坑。

  • url 中的英文句号会被转为斜杠
  • 如果内链的,在子菜单下会在若依中打开
  • 如果内链的,为一级菜单,会在新弹窗打开
  • 如果外链的,一定会新弹窗打开

参考

原文链接:https://juejin.cn/post/7327284192646660150 作者:四叶草会开花

(0)
上一篇 2024年1月24日 上午11:13
下一篇 2024年1月24日 下午4:05

相关推荐

发表回复

登录后才能评论