浏览器可以跨标签页通信吗?我觉得能~

公众号【码农爱摸鱼】,专注于在摸鱼中愉快的工作和学习~

大家都知道,浏览器可以同时打开多个标签页的,而且在实际的使用场景中,打开多个标签页是非常常见的情况。 比如说某个网站的列表页面,点击之后新开标签页展示详情,用户在详情里面进行了操作,列表标签页也需要更新数据。 总的来说:多个标签页之间通信是很有必要的!!!

以下具体聊聊一些通信的方法:

1、localStorage

由于localStorage在同源(同协议、同IP、同端口)下可以实现共享,所以可以利用windw.addEventLister来监听storage内的 数据变化,实现标签页之间的通信效果。 下面来一个简单的例子: a.html

javascript 复制代码
function setStorage(){
  if(!localStorage.getItem('hasStorage')){
    window.open('/b.html')
    localStorage.setItem('hasStorage',1)
  }
  setTimeout(()=>{
    localStorage.setItem('man','摸鱼君')
  },3000)
}

b.html

javascript 复制代码
window.onunload = () =>{
  localStorage.removeItem('hasStorage')
  localStorage.removeItem('man')
}
const app = document.getElementById('app')
window.addEventListener('stroage', e=>{
	const app = document.getElementById('app')
  app.innerText = e.newValue
})

2、window.postMessage(message, targetOrigin, [transfer])

先说说三个参数的意义: message:发送到其他window的信息 targetOrigin:发送信息的目标url,如果没有指定目标url用字符串*表示;但是为了安全,如果你知道目标url建议使用目标url。 transfer(可选参数):是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

window.postMessage只是用来发送消息的,接受消息则要通过监听方法:window.addEventListener('message', receiveMessage, false);实现。 window.addEventListener('message')的回调参数有三个:source(消息源,消息的发送窗口/iframe)、origin(消息源的URI,可以用来验证数据源)、data(发送过来的数据)

window.postMessage与localStorage相比最大的好处就是可以安全地实现跨源通信,使用起来更加方便。

3、webSocket

webSocket是一种网络通讯协议,它是一个全双工通信的协议,也就是说客户端和服务端可以互相通信,享受平等关系(比如说聊天工具)。 那么怎么利用webSocket实现多标签页通信呢?

其实原理也很简单,假如我们a.html和b.html都与服务器建立了websocket连接,那么这两个页面都可以实时接收服务端发来的消息,同时也可以实时向服务端发送消息。 如果a.html更改了数据,向服务端发送一条消息或数据,服务端在将这条消息或数据发送给b.html即可,这样就简单实现了两个标签页之间的通信。

说白了,就是给两个标签页之间提供了一个通信的"中介",两个页面通过这个"中介"来进行通信,所以这种方式也是可以跨源的。

4、sharedWorker

每个前端开发者都知道,js是单线程的。不过在实际的开发过程中,单线程对比于多线程,还是略有不足。 为了弥补单线程的缺陷,webWorker就应运而生了,它就是拿来给js提供多线程环境的。

其中,sharedWorker就是webWorker中的一种,它可以由所有同源页面共享。

其实说白了,sharedWorker就和webSocket的实现原理非常相似,都是发送消息/接收消息,在理解上可以直接理解为webSocket服务器。

简单上点代码: sharedWorker.js

dart 复制代码
const set = new Set()
onconnect = event => {
  const port = event.ports[0]
  set.add(port)


  // 接收信息
  port.onmessage = e => {
    // 广播信息
    set.forEach(p => {
      p.postMessage(e.data)
    })
  }


  // 发送信息
  port.postMessage("sharedWorker发出信息")
}

a.html

xml 复制代码
<script>
  const sharedWorker = new SharedWorker('./sharedWorker.js')
  sharedWorker.port.onmessage = e => {
    console.info("A收到:", e.data)
  }
</script>

b.html

xml 复制代码
<script>
  const sharedWorker = new SharedWorker('./sharedWorker.js')
  let btnB = document.getElementById("btnB");
  btnB.addEventListener("click", () => {
    sharedWorker.port.postMessage("B发出消息")
  })
</script>

这就是一个最简单的a、b两个页面发送消息的实现demo了。SharedWorker初始化完成后:a页面首先收到"A收到:sharedWorker发出信息",然后b页面点击按钮,发送消息给a页面,a页面收到"A收到:B发出消息"。

5、BroadcastChannel

BroadcastChannel接口代理了一个命名频道,可以让指定origin下的任意browsing context 来订阅它。它允许同源的不同浏览器窗口、Tab页、frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。

闲暇的时候写了个简单的demo,有兴趣可以自己跑一下来试试,代码奉上:

index.html

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>摸鱼必备</title>
    <!-- 页面样式 -->
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <!-- 可移动的元素 -->
    <div id="block" class="block" style="left: 0px; top: 0px"></div>
    <!-- 初始化操作按钮 -->
    <div id="init-btn" class="init-btn">
      <span>初始化</span>
    </div>
    <!-- 处理交互逻辑 -->
    <script src="./index.js"></script>
  </body>
</html>

index.css

css 复制代码
* {
  padding: 0;
  margin: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
  user-select: none;
}

html {
  font-size: 16px;
  line-height: 1;
}

body {
  background-color: #EDF2F7;
}

.block {
  display: block;
  position: absolute;
  width: 300px;
  height: 200px;
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07),
              0 2px 4px rgba(0, 0, 0, 0.07),
              0 4px 8px rgba(0, 0, 0, 0.07),
              0 8px 16px rgba(0, 0, 0, 0.07),
              0 16px 32px rgba(0, 0, 0, 0.07),
              0 32px 64px rgba(0, 0, 0, 0.07);
  background-color: #FFF;
  cursor: grab;
}

.init-btn {
  display: inline-block;
  margin: 10px;
  padding: 12px 16px;
  border: 1px solid #2196F3;
  border-radius: 8px;
  background-color: #FFF;
  color: #2196F3;
  font-size: 16px;
  text-align: center;
  cursor: pointer;
}

.init-btn:hover {
  border-color: #FFF;
  background-color: #2196F3;
  color: #FFF;
}

index.js

ini 复制代码
(function () {
  /**
    * @typedef  ChannelMessage
    * @property {string} type
    * @property {any}    data
    */

  /** 可移动元素 */
  let block = document.getElementById('block');

  /** 初始化按钮 */
  let btn = document.getElementById('init-btn');

  /** 跨标签通信广播 */
  let broadcastChannel = new BroadcastChannel('DataTransfer');

  /** 页面相对于窗口边缘的水平距离 */
  let offsetX = 0;

  /** 页面相对于窗口边缘的垂直距离 */
  let offsetY = 0;

  /**
    * @description 转换坐标值
    * @param {'p2s'|'s2p'} type
    * @param {object} coords
    * @param {number} coords.x
    * @param {number} coords.y
    */
  function changeCoords(type, coords) {
    let { x, y } = coords;
    let { screenLeft, screenTop } = window;

    switch (type) {
      // 页面坐标 -> 屏幕坐标
      case 'p2s':
        return {
          x: x + screenLeft + offsetX,
          y: y + screenTop + offsetY,
        };
      // 屏幕坐标 -> 页面坐标
      case 's2p':
        return {
          x: x - screenLeft - offsetX,
          y: y - screenTop - offsetY,
        };
      default:
        console.error('转换失败:类型错误');
        return { x: 0, y: 0 };
    }
  }

  /**
    * @description 处理校准距离
    * @param {object} data
    * @param {number} data.x
    * @param {number} data.y
    */
  function handleAdjustOffset(data) {
    console.log('[AdjustOffset]', data);

    offsetX = data.x;
    offsetY = data.y;

    if (btn) {
      btn.remove();
    }
  }

  /**
    * @description 处理元素坐标更新
    * @param {object} data
    * @param {number} data.x 相对于屏幕的水平坐标
    * @param {number} data.y 相对于屏幕的垂直坐标
    */
  function handleUpdateCoords(data) {
    let { x, y } = changeCoords('s2p', data);
    block.style.left = x + 'px';
    block.style.top = y + 'px';
  }

  /** 初始化元素坐标 */
  function init() {
    /** 元素宽度 */
    let elementW = block.clientWidth;

    /** 元素高度 */
    let elementH = block.clientHeight;

    /** 页面宽度 */
    let windowW = window.innerWidth;

    /** 页面高度 */
    let windowH = window.innerHeight;

    // 将元素移动到页面中心
    block.style.left = Math.round(windowW / 2 - elementW / 2) + 'px';
    block.style.top = Math.round(windowH / 2 - elementH / 2) + 'px';
  }

  /**
    * @description 解析广播消息
    * @param   {MessageEvent<ChannelMessage>} event
    * @returns {ChannelMessage}
    */
  function parseChannelMessage(event) {
    let data = event.data;

    if (data && data.type) {
      return data;
    } else {
      return {
        type: '',
        data: null,
      };
    }
  }

  // 处理元素拖拽
  block.addEventListener('mousedown', function (downEvent) {
    /** 元素相对于屏幕的坐标 */
    let elementCoords = changeCoords('p2s', {
      x: parseInt(block.style.left),
      y: parseInt(block.style.top),
    });

    /** 元素起始水平坐标 */
    let elementX = elementCoords.x;

    /** 元素起始垂直坐标 */
    let elementY = elementCoords.y;

    /** 鼠标起始水平坐标 */
    let cursorX = downEvent.screenX;

    /** 鼠标起始垂直坐标 */
    let cursorY = downEvent.screenY;

    /**
      * @description 处理鼠标移动
      * @param {MouseEvent} moveEvent
      */
    let handleMove = function (moveEvent) {
      let newX = elementX + (moveEvent.screenX - cursorX);
      let newY = elementY + (moveEvent.screenY - cursorY);

      let coords = { x: newX, y: newY };

      // 更新当前页面元素坐标
      handleUpdateCoords(coords);

      // 通知其他页面
      broadcastChannel.postMessage({
        type: 'UpdateCoords',
        data: coords,
      });
    };

    let handleUp = function () {
      window.removeEventListener('mousemove', handleMove);
      window.removeEventListener('mouseup', handleUp);
    };

    window.addEventListener('mousemove', handleMove);
    window.addEventListener('mouseup', handleUp);
  });

  // 处理按钮点击
  btn.addEventListener('click', function (ev) {
    let { pageX, pageY, screenX, screenY } = ev;

    let offsetX = 0,
        offsetY = 0;
    let relativeX = screenX - window.screenLeft;
    let relativeY = screenY - window.screenTop;

    while (relativeX > pageX) {
      offsetX++;
      relativeX--;
    }

    while (relativeY > pageY) {
      offsetY++;
      relativeY--;
    }

    // 更新当前页面数据
    handleAdjustOffset({
      x: offsetX,
      y: offsetY,
    });

    // 通知其他页面更新数据
    broadcastChannel.postMessage({
      type: 'AdjustOffset',
      data: { x: offsetX, y: offsetY },
    });

    // 获取元素相对于屏幕的坐标,用于同步其他页面
    let coords = changeCoords('p2s', {
      x: parseInt(block.style.left),
      y: parseInt(block.style.top),
    });

    // 更新当前页面元素的坐标
    handleUpdateCoords(coords);

    // 通知其他页面更新元素坐标
    broadcastChannel.postMessage({
      type: 'UpdateCoords',
      data: coords,
    });
  });

  // 处理广播消息
  broadcastChannel.addEventListener('message', function (ev) {
    let { data, type } = parseChannelMessage(ev);

    switch (type) {
      case 'AdjustOffset':
        handleAdjustOffset(data);
        break;
      case 'UpdateCoords':
        handleUpdateCoords(data);
        break;
      default:
        break;
    }
  });

  init();
})();

我是摸鱼君,你的【三连】就是摸鱼君创作的最大动力,如果本篇文章有任何错误和建议,欢迎大家留言!

文章持续更新,可以微信搜索 【码农爱摸鱼】关注公众号第一时间阅读。

相关推荐
神仙别闹11 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
aPurpleBerry35 分钟前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安2 小时前
前端第二次作业
前端·css·css3