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

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

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

此类功能有一个叫 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

js 复制代码
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 的函数等。

js 复制代码
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 可以以普通账号密码方式进行表单登录,登录后即可获得授权。

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

js 复制代码
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 ):

js 复制代码
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 问题和参数特殊符号问题。

js 复制代码
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 中的英文句号会被转为斜杠
  • 如果内链的,在子菜单下会在若依中打开
  • 如果内链的,为一级菜单,会在新弹窗打开
  • 如果外链的,一定会新弹窗打开

参考

相关推荐
桂月二二39 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb2 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研2 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
轻口味2 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter