打造基于websocket的iframe插件

前言

这是根据一个需求做的开发总结,适合场景就是类似放在门户网站右边的小按钮,点击弹框载入一个窗口,以及业务需求上接入某个产品页面,然后需要和这个产品做一些数据通信。因此就有了这个插件。

开发雏形难度不大,更多的是细节上一些考虑。

正文

设计上先按照雏形开发,功能完善这个想法进行。

首先肯定是搭建基础骨架,而且现在都ES6开发了,不能用以往ES5那种封装方式来开发。 更主要是配合typescript让开发体验更好一些。

因此一开始


demo/
    index.html
src/
    index.ts
tsconfig.json

这样的结构就可行,index.html 加载的是tsc命令执行后生产的build目录下dist目录里的文件

具体代码如下:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="../build/dist/index.js"></script>
    <title>Document</title>
  </head>
  <body>
  </body>
  <script>
    const test = new TodoPlugin();
    test.init()
  </script>
</html>

之后就是骨架代码index.ts的实现


class TodoPlugin {
  todoPluginId = "";
  todoUrl = "";
  todoBackEndUrl = "";
  constructor() {
  }
  isExist(id: string) {
    return document.getElementById(id);
  }
  remove(id: string) {
    const child = document.getElementById(id);
    child && child.parentNode && child.parentNode.removeChild(child);
  }
  async create(id: string) {
    // 创建容器
    const container = await document.createElement("div");
    container.setAttribute("id", id);
    // 容器设置样式
    container.style.height = "100%";
    container.style.position = "fixed";
    container.style.top = "0";
    container.style.right = "0";
    container.style.left = "0";
    container.style.bottom = "0";
    container.style.zIndex = "1000";
    container.style.backgroundColor = "#595959";
    container.style.opacity = "0.9";
    container.style.display = "none";
    // 将容器设置到tab最前面,让esc事件能生效
    container.tabIndex = 0;
    //容器绑定点击事件
    container.addEventListener(
      "click",
      () => {
        console.log("click");
        this.hide();
      },
      false
    );
    //绑定键盘esc事件
    container.onkeydown = e => {
      console.log("e", e);
      if (e.keyCode == 27) {
        this.hide();
      }
    };
    // 创建iframe
    const iframe = await document.createElement("iframe");
    iframe.src = this.todoUrl;
    iframe.setAttribute("id", id + "Iframe");
    // iframe 设置样式
    iframe.style.height = "80%";
    iframe.style.width = "80%";
    iframe.style.margin = "10%";
    // 去掉边框
    iframe.scrolling = "no";
    iframe.frameBorder = "no";
    // 载入到页面
    await container.appendChild(iframe);
    await document.body.appendChild(container);
  }
  init(appId: string, username?: string) {
    // 判断是否已有iframe
    if (this.isExist(this.todoPluginId)) {
      console.log("节点存在");
      // 删除节点
      this.remove(this.todoPluginId);
      // 重新初始化
      this.init(appId, username);
    } else {
      console.log("节点不存在");
      // 创建节点
      this.create(this.todoPluginId);
    }
  }
  // 展示iframe
  show() {
    const container = document.getElementById(this.todoPluginId);
    container ? (container.style.display = "block") : "";
    // 防止通过按钮执行show函数,导致焦点在button上使esc事件失效,因此手动聚焦。
    container ? container.focus() : "";
  }
  // 隐藏iframe
  hide() {
    const container = document.getElementById(this.todoPluginId);
    container ? (container.style.display = "none") : "";
  }
  // 销毁iframe
  destroy() {
    const iframe = document.getElementById(this.todoPluginId);
    iframe ? document.body.removeChild(iframe) : null;
  }
  // 重新初始化iframe
  refresh() {
    // @ts-ignore
    const iframe = document.getElementById(this.todoPluginId + "Iframe");
    // @ts-ignore
    iframe.src = iframe.src;
  }
}

可能一开始看到代码会有点迷,怎么骨架有怎么多方法,原因是我们是基于iframe来加载目标应用,那么对iframe涉及到的操作我们都需要考虑。用原生写法是为了兼容和可扩展性考量。

这里也有一些设计上的考量,那就是对已有的重复节点需要做处理。具体流程图如下:

wechatimg163.png

这个比较简单。重点讲一下按键绑定的过程。

这是基于很常见的交互需求,点击iframe外部或者右上角叉叉可以隐藏iframe,直接按esc,也可以隐藏。点击关闭很好解决,隐藏需要注意的是要将插件父节点的tabIndex设置为0,这样浏览器才会帮你把默认的焦点放在父节点上。

创建完骨架后,已经可以载入目标应用了,此时就提到了上述的需求,我们需要和目标应用做通讯操作,而iframe中最方便也就是websocket了。

我们需要再目标应用中加入一个监听器来监听来自插件的询问,以及将各个功能模块抽象,以便于插件提出的功能实现。

而插件也需要监听来自目标程序的回应,以及一些秘钥的交换。

首先我们在目标程序初始化的生命周期中中加入如下代码:


      // 启动 PostMessage 监听信息
      window.addEventListener(
        'message',
        e => {
          if (window.location.origin !== e.origin) {
            console.log('监听到消息:', e, e.data);
            // @ts-ignore
            this.postObject = e;
            console.log('this.postObject', e);
            if(e.data.type === 'sendTokenRequest') {
              // @ts-ignore
              e.source.postMessage({type: 'token', token: window.SERVER_DATA && window.SERVER_DATA.accessToken} , e.origin);
            }
            if(e.data.type === 'selectTodo') {
              this.selectTodo(e.data.id);
            }
          }
        },
        false
      );

这里简单加了两个指令,用户插件通知目标程序做些什么。

首先是秘钥的交换。插件一开始是什么都没有的,因此初始化的时候它会发个请求让目标应用去将秘钥发回。

而插件的监听代码如下:


window.addEventListener(
  "message",
  e => {
    if (window.location.origin !== e.origin) {
      console.log("插件获取返回数据:", e, e.data);
      switch(e.data.type){
        case 'token': 
          window["todoPluginToken"] = e.data.token;
          break;
      }
    }
  },
  false
);

到这里,简单的通讯就建立了。

因为插件多个地方需要用到和目标应用通讯,因此需要把这个函数抽象出来


  sendMessage(id, data) {
    // 发送消息
    // @ts-ignore
    const iframeWindow = document.getElementById(id + "Iframe").contentWindow;
    iframeWindow.postMessage(data, "*");
  }

至此,简单的小插件就完成了。

Table of Contents