浏览器标签页交互技术方案(JS)

1,469 阅读4分钟

开篇

在日常业务开发中,时常会遇到这样一场景:浏览器标签页之间进行通信交互。

比如有一个 list 页面,列表中的每一项数据经过点击后,以新打开浏览器 Tab 标签页方式显示这条数据的 detail 页面;

detail page 经过编辑后,可通过点击右上角 更新 按钮,关闭当前页面,进入并通知 list page 重新请求更新列表数据。

这里,就涉及到两个交互:

  1. 如何关闭当前 Tab 标签页,进入其他标签页;
  2. 如何通知其他标签页去更新数据。

下面,我们一起了解一下实现过程。

标签页之间通信

首先,我们来看下两个标签页之间如何进行通信,即:关闭 detail page 后,如何让 list page 接到通知去做更新处理。

我们定义两个页面:list.htmldetail.html

list.html 页面中有一个打开 detail.html 的按钮:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <button onclick="openDetail()">打开 detail 页面</button>
  <script>
    function openDetail() {
      window.open('file://detail.html');
    }
  </script>
</body>
</html>

detail.html 页面中有一个返回 list.html 的按钮:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <button onclick="backList()">返回 list 页面</button>
  <script>
    function backList() {
      // ...
    }
  </script>
</body>
</html>

当我们点击 detail.html 中的 返回 list 页面 按钮后,期望在 list.html 页面中能够接收到通知。

在两个页面同源的情况下,可以借助于 localStorage 来实现。

通常我们使用 localStorage 作为数据本地存储,除了这一特性外,它还提供了监听事件来监听 localStorage 中数据的变化。

因此我们可以:点击 返回 list 页面 时给 localStorage 中设置一条数据,在 list.html 中可以监听这条数据去做更新处理。

具体实现如下:

// detail.html
function backList() {
  localStorage.setItem("reload-list", Math.random());
}

// list.html
window.addEventListener("storage", (e) => {
  if (e.key === 'reload-list') {
    alert('重新请求接口,刷新 list 数据!');
  }
})

关闭当前 Tab 标签页

当点击 detail.html 右上角更新按钮后,我们期望关闭 detail.html 页面,回到 list.html 页面。

如何关闭当前标签页呢?

其实浏览器全局对象 window 提供了这个方法:window.close()

但是这个方法生效需要有一个前提:当前页面必需以弹出窗口的方式打开,如通过 window.open() 打开当前页。

如果以 非弹出窗口 方式打开(复制链接直接在地址栏打开),调用 close() 时会在控制台看到如下警告:

Scripts may close only the windows that were opened by them.

在上面 list.html 中点击按钮是通过 window.open 打开 detail.html,因此在 detail.html 中添加关闭逻辑可以生效:

function backList() {
  localStorage.setItem("reload-list", Math.random());
  window.close();
}

那么,非弹出窗口 方式打开 detail.html 此时该如何处理呢?这一场景我们可能就会选择:采用重定向方式在 detail.html 页面中去打开 list.html 页面。

那么,如何判断当前页面是以 弹出窗口 还是 非弹出窗口 打开呢?

通过 window.opener 变量可以解决。

如果新标签页面是当前页面以 window.open 方式打开,那么 window.opener 则指向当前页面,即 list.html

如果新标签页面不是采用 window.open 方式打开,window.opener 则为 null。

我们改写逻辑如下:

function backList() {
  localStorage.setItem("reload-list", Math.random());
  if (window.opener) {
    window.close();
  } else {
    alert('无法关闭,执行重定向 list 逻辑');
  }
}

打开指定 Tab 标签页

其实,上面在关闭 detail.html 时,可能会有一个问题,list.htmldetail.html Tab 标签页之间可能存在其他页面,在关闭了 detail.html 后,并未将标签页打开在 list.html 中。

打开 list.html 这一逻辑如何实现呢?

其实,window.open() 除了使用第一参数传递 链接 url 能够打开页面外,第二参数 name 可以打开已存在的 Tab 标签页。

我们在 list.html 页面为其设置 name 属性:

window.name = "list";

detail.html 页面中可以通过打开 name 的方式来切换 Tab 标签页。

function backList() {
  localStorage.setItem("reload-list", Math.random());
  if (window.opener) {
    window.open('', 'list');
    window.close();
  } else {
    alert('无法关闭,执行重定向 list 逻辑');
  }
}

判定是否已打开指定 Tab 标签页

可能会有这么一场景:list.html 页面打开 detail.html 页面后,首先将 list.html 给关闭了,然后在 detail.html 中点击返回关闭当前页面。

此时大家可能会想到结论:当点击更新时,detail.html 也被关闭了,这就并没有实现,回到 list.html 这一场景。

好在,如果 list.html 被关闭了,在 detail.html 中拿到的 window.opener 会被置为 null,因此会命中重定向 list.html 页面逻辑。

function backList() {
  localStorage.setItem("reload-list", Math.random());
  if (window.opener) { // window.opener === null
    // ...
  } else {
    alert('无法关闭,执行重定向 list 逻辑');
  }
}

但是,如果当前 Tab 标签页中有 list.html,而 detail.html 是通过复制链接在浏览器 url 地址栏打开的,这将无法做到关闭这个 detail.html,因为它并非采用 弹窗方式 打开。

最后

感谢阅读,如有不足之处,欢迎指正 👏 。