最近需要实现一个系统,内含几个子系统,每个子系统有各自的功能、菜单。
比如某个系统叫 信息化设备运维管理
,在这里实现设备的组网、检测、统计等。
此类功能有一个叫 zabbix 的程序,已经做了很多事情。而这些事情也正是客户所想要的(当然你懂的,也并不全想要),那要从0开发?二开?
不可能!决定不可能!客户年前就要。经协商,我们可以把那些客户想要的功能,集成到我们的系统里面。
所以这件事情可以简单抽象为:需求是在页面内集成三方页面(当前是集成 zabbix 的两个页面),会有自动登录功能。
实现结果
提示:由于动图分辨率较高,特把帧率调得比较低,所以看起来比较卡。
集成前的三方系统:
- 需要登录
- 系统里有菜单
- 有退出功能
集成后的几个三方系统:
- 直接进入某页面无需登录
- 没有菜单等其他无关元素
- 有 loading 功能
方案
- 微前端
力求运行上沙箱完整性,通信上的便利性,附加一些优化,例如预加载、缓存、让弹窗能弹在宿主上。
- iframe
天然的沙箱,使用符合浏览器实现的安全规则和通信方式,所有元素例如弹窗都只限定在 iframe 中。
- 自动登录
可以使用单点登录、模拟登录等方式。
微前端
- 选择
有 qiankun、Micro App、无界等前端框架可以选择。
- 测试
使用无界和Micro App实现了页面集成,但qiankun总是报错方法没有注入(要求注入一些生命周期到要嵌入的三方系统中)。
- 结果
对于微前端方案,有以下几点问题:
- 兼容性无法保证,例如子系统某些定位元素会跑到父系统上
- 复杂,出现兼容性问题到底是哪个环境的问题
- 如果要与子系统通信,也需要跨域
- 出现问题时某此框架也可以回退到 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 中的英文句号会被转为斜杠
- 如果内链的,在子菜单下会在若依中打开
- 如果内链的,为一级菜单,会在新弹窗打开
- 如果外链的,一定会新弹窗打开
参考
- Zabbix单点登录 open.bccastle.com/app_integra...
- ZABBIX实现单点登录 www.congao.com.cn/?techdev/30...
- 微前端方案对比 www.jiangyh.cn/2022/12/25/...
- 除了 Qiankun, 这些微前端框架或许更适合你 juejin.cn/post/712188...
- 为跨源请求设置cookie stackoverflow.com/questions/4...
- Cookie 和 Iframe medium.com/trabe/cooki...
- SameSite Cookie 说明 web.dev/articles/sa...