哈喽大家好啊,我是广州小井。最近做需求碰到个可以通过劫持 WebSocket 来解决问题的场景,整个案例还是挺有意思的,于是我赶紧来发一篇水文。
场景
最近做个需求是关于要通过一个第三方服务 kubevela 提供的 web 版 shell 窗口交互进入容器的。简单来说就是通过他们的前端 shell 界面,输入一定的进入容器的 shell 命令再回车,进入到具体的容器中…
遇到问题
如果完全采用第三方的交互行为来进入容器也是完全可行的,并不存在什么问题。但是 vela 基于一定的场景设计出当前的交互效果(我个人是这么理解的)并不完全符合我们内部使用的一个期望。比如说当我要进入某一个容器时,它那边设计有这几个步骤:
首先,弹出弹窗,展示一段 shell 命令(可复制),如下图:
其次,点击 Open Cloud Shell
按钮打开 shell 界面:
最后,粘贴第一步的 shell 命令再回车,进入到具体的容器:
一番操作完成后,我们终于进入到了容器里。但这几个步骤,对于我当前面临的业务场景是冗余的。在目前的内部需求设计中,从我们自己的前端界面入口到进入到对应的容器是一个一一对应的关系,所以完全可以将上述步骤做一个合并整合,一键式进入到对应的容器内部。
思路分析
首先大致讲解一下当前需求中,整体功能模块的划分情况以方便大家理解场景。
如上图,在当前的实现逻辑中,进入容器这一块的具体业务流程大概成这样:
- 在自己的前端项目中点击“进入容器之”之类的按钮,然后会开始请求第三方服务的接口(这里通过 nginx 转发解决跨域问题,因为一会要操作
iframe
里的window
),比如权限、初始化等接口 - 最终可以拿到第三方返回的一个
Content-type:text/html
的 html 文件(也就那是 shell 窗口) - 在这个 shell 页面中,它会执行一段自己的 script 来跟第三方的后端进行一个 socket 连接(这里也给我们用 nginx 来转发了)以进行 shell 命令的交互
上述流程完成后,我们就可以在这个返回的 html 中输入一些 shell 命令来进入容器,然后进行各种命令行操作了。
当下摆在眼前的问题就一个,那就是如何让这个返回的 html 页面可以自动进入容器。或者说怎么样模拟人工输入命令、回车的这个动作,在用户打开这个 shell 界面的时候就已经进入到容器内部了。
这时候我大概就两种思路:
- 第一,自己实现一个 shell 窗口的界面,然后跟自己跟第三方连通 socket 来交互;
- 第二,直接对这个返回的 html 动手,让它可以自动执行进入容器的命令然后进入容器。
那么这两种方案的优缺点都非常的明显啊。第一点的缺点:开发成本无疑更大,不仅要找到合适的组件,还要自己实现一些基础的业务逻辑、建 sokcet 链接等等;那优点呢也很明显,那就是自由度最高,操控程度最强,不管什么的需求都能给他实现了!毕竟这个时候自己的页面,可以完全拿捏。那第二点呢,就跟第一点完全相反,开发成本低,但是业务的机动性就差,可支配程度低,不一定可以适配所有的需求。
秉承着摸鱼第一,工作第二效率开发,快速交付的优良传统,我当然是选择第二点啦。大致的实现思路是将这个 html 以 iframe
的方式嵌入到自己的前端项目中,然后通过重写(劫持) iframe
中 window.WebSocket
来实现干预它,发送一些我想发送的数据。
劫持 WebSocket
思路有了,干起活来就简单轻松了。既然是要劫持 iframe
中的 window.WebSocket
,那就一定要找准时只因!
为什么说要找准时只因?因为前文有提到 iframe
中的 html 会有自己的 script
要执行,比如说它需要初始化 socket 链接!所以我们的劫持时只因只需要在既能够获取到 iframe
的 window
对象,又能赶在它执行建立 socket 连接之前就行了!
所以我选择在 iframe
的 onload
阶段来进行 WebSocket
的劫持。劫持的代码其实非常简单,如果大家理解过 Vue2 对数组方法的重写,那这里具体的代码实现完全可以不用看了,对你来说小菜一碟。
这里先贴个 MDN-WebSocket 的文档,有需要详细了解的可以戳一戳
ifr.onload = function () {
// 保留原本的 WebSocket 构造器
const ws = ifr.contentWindow.WebSocket
// 重写 iframe 中的 WebSocket
ifr.contentWindow.WebSocket = function (url, protocols) {
// 创建一个 socket 实例
const wsInstance = protocols ? new ws(url, protocols) : new ws(url);
// 连接成功后
wsInstance.addEventListener('open', function() {
// 干点啥事好呢?
})
// 从服务器接受到信息
wsInstance.addEventListener('message', function() {
// 干点啥事好呢?
})
...
// 最后返回这个我们劫持后的 socket 示例
return wsInstance;
};
// 将原本 WebSocket 的静态属性还原到重写的 WebSocket 中
Object.keys(ws).forEach(key => {
ifr.contentWindow.WebSocket[key] = ws[key]
})
}
简单来看上述代码做了什么:
-
重写了
iframe
中的WebSocket
构造器。其实就是一个装饰器,在重新实现的WebSocket
构造器中,我们对 socket 实例进行包装。这样以来,我们可以在一些事件中做一点我们自己的事情。如下截图的事件节点我们都可以插一脚自己的代码! -
还原
WebSokcet
的静态属性,如下这些:
ok,非常简单的代码编写就已经实现了对 iframe
中 WebSokcet
的劫持。
实现效果
这里,我们一起看看劫持后的实现效果。比如我们把劫持到的事件进行一些 console
输出:
wsInstance.addEventListener('open', function() {
// 干点啥事好呢?
console.log('onopen >>> 干点啥事好呢?')
})
// 从服务器接受到信息
wsInstance.addEventListener('message', function() {
// 干点啥事好呢?
console.log('onmessage >>> 干点啥事好呢?')
})
那当我打开点击进入容器时,会请求到这个第三方返回的 html,并且我把 html 放置到当前页面的 iframe
中,这时候打开控制台可以看到:
如上图所示,已经成功的输出了我们的 console
内容!!!那这个时候,我们再来看看 network 中 ws 的具体的请求响应情况:
如上图可以看出,第三方返回的 html 在 iframe 页面加载完后,向服务器 send 了两条数据(我猜测应该是一些初始化内容吧,其中一条发送了个空对象 {}
数据),然后我们可以看到接收到了四条来自服务器的消息通知!那再对上前一张截图中的 onmessage >>> 干点啥事好呢?
总共输出了四次!
完美,成功劫持!接下来我就可以通过在不同的事件中做一些操作来满足当前的业务需求了~