📞前端通信百宝箱📱

发布于:2024-04-28 ⋅ 阅读:(17) ⋅ 点赞:(0)

f0b7222a7fe3437381651484a898cd31.jpg

跨标签通信

image.png

BroadcastChannel

BroadcastChannelHTML5提供的一种前端通信方式,它可以在允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间进行消息广播和接收。

使用 BroadcastChannel 可以轻松实现浏览器内的跨页面通信,而不需要使用复杂的 Web Socket 或其他通信方式。

⚡基本用法⚡

// 创建一个名为 'myChannel' 的 BroadcastChannel
const channel = new BroadcastChannel('myChannel');

// 发送消息:向所有监听了相同频道的 BroadcastChannel 对象发送一条消息,消息内容可以是任意类型的数据
channel.postMessage('Hello from sender!');

// 监听来自 'myChannel' 的消息
channel.onmessage = event => {
  console.log('Received message:', event.data);
};

// 或者使用addEventListener监听
channel.addEventListener('message', (event) => {
  console.log('Received message:', event.data);
});

// 关闭 BroadcastChannel,不再接收新的消息
channel.close();

当频道收到一条消息时,会在关联的 BroadcastChannel 对象上触发 MessageEvent 事件,这个事件包含以下属性:

  1. data: 包含了从发送者发送的消息数据。
  2. origin: 表示消息发送者的源,即发送消息的页面的源。
  3. ports: 一个数组,表示与消息关联的 MessagePort 对象(如果有)。这主要用于支持 MessagePort 的消息传递,以实现更复杂的通信模式。

这些属性使得开发者能够在接收到消息时获取消息内容以及相关的上下文信息,从而更好地处理和响应接收到的消息。

localStorage

使用 localStorage 结合 StorageEvent 事件监听可以实现前端跨标签通信的方式。

localStorage.getItem("data-time");  
localStorage.setItem("data-time", Date.now());  
localStorage.clear("data-time");  

存储数据受同源策略限制,即只有相同域名、协议和端口的页面才能相互访问各自的数据

当一个标签页更新了 localStorage 中的数据时,其他所有页面可以通过StorageEvent监听到这些变化并做出响应。

StorageEvent 是一个 Web API 中定义的事件类型,用于在浏览器的 localStoragesessionStorage 对象发生变化时触发。

下面详细解释一下 StorageEvent

storage 事件属性⚡

主要有:

  1. key:发生变化的键名。
  2. oldValue:变化前的值。
  3. newValue:变化后的值。
  4. url:导致存储区域变化的文档的 URL。
  5. storageArea:存储区域的类型,可以是 localStoragesessionStorage 的一个实例。

下面是一个简单的示例,演示如何使用 StorageEvent 监听 localStorage 的变化:

window.addEventListener('storage', function(event) {
  if (event.key === 'data-time') {
    console.log('Storage event detected.');
    console.log('Old value:', event.oldValue);
    console.log('New value:', event.newValue);
    console.log('URL:', event.url);
    console.log('Storage area:', event.storageArea);
  }
});

这段代码会在监听到 localStorage 中名为 "data-time" 的数据发生变化时触发,然后打印出变化前的值、变化后的值、发生变化的页面 URL 和存储区域的类型。

通过 StorageEvent,可以实现跨页面通信,当一个页面修改了 localStorage 中的数据,其他页面可以通过监听 StorageEvent 来获取这些变化并做出相应的响应。

postMessage-可跨域

postMessage() 是一个 HTML5 提供的 API,用于实现不同窗口或 iframe 之间的跨域通信。

它允许你发送消息,同时在安全的环境下接收消息。主要用途包括:

  1. 跨窗口通信 可以在一个浏览器窗口或标签页中的文档和来自另一个窗口的脚本进行通信。
  2. 跨域通信 可以在不同域名、协议或端口的页面之间进行通信,只要通过 postMessage() 进行通信的窗口已经合法地互相引用。

⚡使用教程⚡

  1. 发送消息 在发送消息的窗口或 iframe 中调用 postMessage() 方法,将消息和目标窗口的源(origin)发送给目标窗口。
targetWindow.postMessage(message, targetOrigin);

targetWindow 是对其他窗口的引用:

1.例如 iframe 的 contentWindow 属性

2.返回的窗口对象const opener = window.open(xxURL);

3.嵌套的窗口对象window.parent或者是等

1.message(消息): 要发送的消息内容。这可以是一个简单的字符串、数字、对象或数组等。

2.targetOrigin(目标源): 表示目标窗口的源(origin),即消息接收方所在文档的源。

targetOrigin(目标源)可以是一个 URL 字符串或 *。如果你指定了具体的源,那么只有来自该源的窗口才能接收到消息。如果将目标源设为 *,则表示消息可以被发送到任何源,但这样做可能存在安全风险,因此建议尽量避免使用 *,而是指定具体的目标源。

  1. 接收消息 在接收消息的窗口或 iframe 中,通过监听 message 事件来接收消息,并在事件处理程序中处理消息。
window.addEventListener('message', receiveMessage, false);
​
function receiveMessage(event) {
  // event.data 包含收到的消息
  // event.origin 包含消息的来源
  // event.source 对发送消息的窗口对象的引用; 你可以使用此来在具有不同 origin 的两个窗口之间建立双向通信
}

🐷注意事项🐷

  • 安全性: 始终在发送消息时指定目标窗口的源(origin),以确保只有来自期望的源的消息才会被接收和处理。
  • 兼容性: postMessage() 在现代浏览器中有很好的支持,但在处理 IE8 和更早版本时可能需要注意一些兼容性问题。
  • 性能: 避免在短时间内频繁地发送大量的消息,以免影响性能。

postMessage() 是实现跨窗口通信的一种简单且强大的方法,使得在 Web 应用程序中实现各种交互和集成变得更加容易。

🍋示例Iframe🍋

image.png image.png

// a.html
<!DOCTYPE html>
<html lang="en">
  <body>
    <h1>Page A</h1>
    <div
      id="colorBoxA"
      style="width: 200px; height: 200px; border: 1px solid black"
    ></div>
    <button id="changeColorBtnA">Change Color</button>
    <iframe id="iframeB" src="b.html" width="400px" height="400px"></iframe>
    <script>
      const colorBoxA = document.getElementById("colorBoxA");
      const changeColorBtnA = document.getElementById("changeColorBtnA");
      const iframeB = document.getElementById("iframeB");

![10692874f4c64ba4b853579ff4136f9a.jpg](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58bf5a5653c14cd9b56cb0191744c650~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1920&h=1080&s=467437&e=jpg&b=38341e)
      colorBoxA.style.backgroundColor = "#999";

      changeColorBtnA.addEventListener("click", function () {
        const randomColor = getRandomColor();
        colorBoxA.style.backgroundColor = randomColor;
        iframeB.contentWindow.postMessage(randomColor, "*");
      });

      window.addEventListener("message", function (event) {
        colorBoxA.style.backgroundColor = event.data;
      });

      function getRandomColor() {
        return "#" + Math.floor(Math.random() * 16777215).toString(16);
      }
    </script>
  </body>
</html>
  
// b.html
<!DOCTYPE html>
<html lang="en">
  <body>
    <h1>Iframe B</h1>
    <div
      id="colorBoxB"
      style="width: 200px; height: 200px; border: 1px solid black"
    ></div>
    <button id="changeColorBtnB">Change Color</button>
    <script>
      const colorBoxB = document.getElementById("colorBoxB");
      const changeColorBtnB = document.getElementById("changeColorBtnB");

      colorBoxB.style.backgroundColor = "#999";

      changeColorBtnB.addEventListener("click", function () {
        const randomColor = getRandomColor();
        colorBoxB.style.backgroundColor = randomColor;
        window.parent.postMessage(randomColor, "*");
      });

      window.addEventListener("message", function (event) {
        colorBoxB.style.backgroundColor = event.data;
      });

      function getRandomColor() {
        return "#" + Math.floor(Math.random() * 16777215).toString(16);
      }
    </script>
  </body>
</html>

实现了两个标签页之间的双向通信和交互。当你在 a.html 页面上点击 "Change Color" 按钮时,b.html 页面的颜色盒子也会随之改变。反之亦然。

🍋示例打开新窗口🍋


// a.html
<!DOCTYPE html>
<html lang="en">
  <body>
    <h1>Page A</h1>
    <div
      id="colorBoxA"
      style="width: 200px; height: 200px; border: 1px solid black"
    ></div>
    <button id="changeColorBtnA">Change Color</button>
    <script>
      let page2 = null;
      const colorBoxA = document.getElementById("colorBoxA");
      const changeColorBtnA = document.getElementById("changeColorBtnA");

      colorBoxA.style.backgroundColor = "#999";

      changeColorBtnA.addEventListener("click", function () {
        const randomColor = getRandomColor();
        colorBoxA.style.backgroundColor = randomColor;
        if (!page2) {
          page2 = window.open("b.html", "page 2");
        }
        page2.postMessage(randomColor, "*");
      });

      window.addEventListener("message", function (event) {
        colorBoxA.style.backgroundColor = event.data;
      });

      function getRandomColor() {
        return "#" + Math.floor(Math.random() * 16777215).toString(16);
      }
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <body>
    <h1>Iframe B</h1>
    <div
      id="colorBoxB"
      style="width: 200px; height: 200px; border: 1px solid black"
    ></div>
    <button id="changeColorBtnB">Change Color</button>
    <script>
      const page1 = window.opener;
      const colorBoxB = document.getElementById("colorBoxB");
      const changeColorBtnB = document.getElementById("changeColorBtnB");

      colorBoxB.style.backgroundColor = "#999";

      changeColorBtnB.addEventListener("click", function () {
        const randomColor = getRandomColor();
        colorBoxB.style.backgroundColor = randomColor;
        page1.parent.postMessage(randomColor, "*");
      });

      window.addEventListener("message", function (event) {
        colorBoxB.style.backgroundColor = event.data;
      });

      function getRandomColor() {
        return "#" + Math.floor(Math.random() * 16777215).toString(16);
      }
    </script>
  </body>
</html>

  
// b.html
<!DOCTYPE html>
<html lang="en">
  <body>
    <h1>Iframe B</h1>
    <div
      id="colorBoxB"
      style="width: 200px; height: 200px; border: 1px solid black"
    ></div>
    <button id="changeColorBtnB">Change Color</button>
    <script>
      const colorBoxB = document.getElementById("colorBoxB");
      const changeColorBtnB = document.getElementById("changeColorBtnB");

      colorBoxB.style.backgroundColor = "#999";

      changeColorBtnB.addEventListener("click", function () {
        const randomColor = getRandomColor();
        colorBoxB.style.backgroundColor = randomColor;
        window.parent.postMessage(randomColor, "*");
      });

      window.addEventListener("message", function (event) {
        colorBoxB.style.backgroundColor = event.data;
      });

      function getRandomColor() {
        return "#" + Math.floor(Math.random() * 16777215).toString(16);
      }
    </script>
  </body>
</html>

Service Worker

Service Worker 是一种浏览器 API,用于拦截和处理网络请求管理缓存以及实现离线访问等功能。它是一种在浏览器背后独立运行的 JavaScript 脚本,能够在用户关闭网页后继续运行,可以用于实现诸如离线访问推送通知等功能。

由于它可以在网页不打开的情况下运行,因此可以被用于实现像推送通知这样的功能,让用户在没有打开网页的情况下也能接收到通知。

一种常见的做法是使用Service Worker的消息传递功能,通过 postMessage() 方法在不同的页面之间发送消息。

⚡通信流程⚡

前端页面与 Service Worker 通信的简单流程如下:

  1. 注册 Service Worker 页面加载时,通过 navigator.serviceWorker.register() 方法注册一个 Service Worker。这会让浏览器在后台运行一个脚本,用于拦截网络请求、缓存资源等任务。
  2. 页面发送消息给 Service Worker: 页面上的某个事件(比如按钮点击)触发时,通过 navigator.serviceWorker.controller.postMessage() 方法向 Service Worker 发送消息。消息可以是任意数据,比如对象、字符串等。
  3. Service Worker 接收消息: Service Worker 使用 self.addEventListener('message', ...) 方法监听来自页面的消息。当接收到消息时,可以进行相应的处理,比如打印消息内容、修改缓存、发送消息给其他客户端等。
  4. Service Worker 回复消息给页面(可选): 如果需要,Service Worker 可以使用 event.source.postMessage() 方法向页面发送消息。这样页面就能收到来自 Service Worker 的响应。
  5. 页面接收来自 Service Worker 的消息: 页面可以使用 navigator.serviceWorker.addEventListener('message', ...) 方法监听来自 Service Worker 的消息。当接收到消息时,可以进行相应的处理,比如更新页面内容、显示通知等。

这样,前端页面与 Service Worker 就能通过消息传递进行通信,实现各种功能,比如离线缓存、后台同步、推送通知等。

🍋示例🍋

image.png image.png

// a.html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端通信 Service Worker 示例</title>
  </head>

  <body>
    <button id="sendDataBtnA">从 A 发送数据</button>
    <ol id="receivedDataListA"></ol>

    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker
          .register("worker.js")
          .then(() => {
            console.log("Service Worker 注册成功");
          })
          .catch((error) => {
            console.error("Service Worker 注册失败:", error);
          });
      }

      const sendDataBtnA = document.getElementById("sendDataBtnA");
      const receivedDataListA = document.getElementById("receivedDataListA");

      sendDataBtnA.addEventListener("click", () => {
        const data = { source: "page A", time: new Date().toLocaleString() };
        if ("serviceWorker" in navigator) {
          navigator.serviceWorker.controller.postMessage(data);
        }
      });

      navigator.serviceWorker.addEventListener("message", (event) => {
        const receivedData = event.data;
        const listItem = document.createElement("li");
        listItem.textContent = `${
          receivedData.source === "page A" ? "发送数据" : "接收数据"
        } ${receivedData.source} ${receivedData.time}`;
        receivedDataListA.appendChild(listItem);
      });
    </script>
  </body>
</html>

//b.html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端通信 Service Worker 示例</title>
  </head>

  <body>
    <button id="sendDataBtnB">从 B 发送数据</button>
    <ol id="receivedDataListB"></ol>

    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker
          .register("worker.js")
          .then(() => {
            console.log("Service Worker 注册成功");
          })
          .catch((error) => {
            console.error("Service Worker 注册失败:", error);
          });
      }

      const sendDataBtnB = document.getElementById("sendDataBtnB");
      const receivedDataListB = document.getElementById("receivedDataListB");

      sendDataBtnB.addEventListener("click", () => {
        const data = { source: "page B", time: new Date().toLocaleString() };
        if ("serviceWorker" in navigator) {
          navigator.serviceWorker.controller.postMessage(data);
        }
      });

      navigator.serviceWorker.addEventListener("message", (event) => {
        const receivedData = event.data;
        const listItem = document.createElement("li");
        listItem.textContent = `${
          receivedData.source === "page B" ? "发送数据" : "接收数据"
        } ${receivedData.source} ${receivedData.time}`;
        receivedDataListB.appendChild(listItem);
      });
    </script>
  </body>
</html>

// worker.js
self.addEventListener("message", function (e) {
  e.waitUntil(
    self.clients.matchAll().then(function (clients) {
      if (!clients || clients.length === 0) {
        return;
      }
      clients.forEach(function (client) {
        client.postMessage(e.data);
      });
    })
  );
});

SharedWorker

SharedWorker 是一种特殊类型的 Web Worker,它可以在多个浏览器上下文(如不同的窗口、标签页或框架)之间共享数据和通信。与普通的 Web Worker 不同,SharedWorker 可以被多个客户端同时连接,从而实现了数据和通信的共享。

通过 SharedWorker,不同的页面可以共享相同的后台计算资源,这在以下情况下非常有用:

  1. 共享状态和数据 多个页面可以通过 SharedWorker 共享状态和数据,实现数据同步和共享内存。
  2. 跨页面通信 SharedWorker 充当中介,允许多个页面之间进行实时通信,而无需通过服务器中转。
  3. 资源共享 由于 SharedWorker 可以在多个页面之间共享,因此可以减少浏览器中的资源使用,提高性能和效率。

🐷注意事项🐷

在前端通信中,SharedWorker 的使用方式与其他类型的 Web Worker 类似,但需要注意以下几点:

  • 跨域通信限制 SharedWorker 通常受同源策略限制,即只能在相同的域名下运行。
  • 兼容性:SharedWorker 目前并不是所有浏览器都支持,主要支持 Chrome、Firefox 和 Opera。
  • 安全性: 在使用 SharedWorker 进行通信时,需要注意确保通信的安全性。避免传输敏感信息或在通信过程中存在安全漏洞,可以通过加密、认证等方式提升通信安全性。
  • 消息传递:使用 postMessageonmessage 进行消息传递时,需要注意消息格式和内容。建议使用结构化的消息格式,如 JSON,以便于解析和处理。
  • 性能和资源消耗:由于 SharedWorker 是独立运行的,因此可能会带来一定的性能和资源消耗。需要合理设计和管理 SharedWorker 的使用,以确保应用的整体性能。
  • 值得注意的是,如果你采用 addEventListener 来接收 message 事件,那么在主线程初始化SharedWorker() 后,还要调用 SharedWorker.port.start() 方法来手动开启端口。但是,如果采用 onmessage 方法,则默认开启端口,不需要再手动调用SharedWorker.port.start()方法

总的来说,SharedWorker 提供了一种便捷的方式,允许多个前端页面之间共享数据和进行通信,从而实现更复杂和灵活的前端应用。

🍋示例🍋

image.png image.png

// a.html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端通信 SharedWorker 示例</title>
  </head>

  <body>
    <button id="sendDataBtnA">从 A 发送数据</button>
    <ol id="receivedDataListA"></ol>

    <script>
      const sharedWorker = new SharedWorker("worker.js");

      const sendDataBtnA = document.getElementById("sendDataBtnA");
      const receivedDataListA = document.getElementById("receivedDataListA");

      sendDataBtnA.addEventListener("click", () => {
        const data = { source: "page A", time: new Date().toLocaleString() };
        sharedWorker.port.postMessage(data);
      });

      sharedWorker.port.onmessage = (event) => {
        const receivedData = event.data;
        const listItem = document.createElement("li");
        listItem.textContent = `${
          receivedData.source === "page A" ? "发送数据" : "接收数据"
        } ${receivedData.source} ${receivedData.time}`;
        receivedDataListA.appendChild(listItem);
      };
    </script>
  </body>
</html>

// b.html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端通信 SharedWorker 示例</title>
  </head>

  <body>
    <button id="sendDataBtnB">从 B 发送数据</button>
    <ol id="receivedDataListB"></ol>

    <script>
      const sharedWorker = new SharedWorker("worker.js");
      sharedWorker.port.start();

      const sendDataBtnB = document.getElementById("sendDataBtnB");
      const receivedDataListB = document.getElementById("receivedDataListB");

      sendDataBtnB.addEventListener("click", () => {
        const data = { source: "page B", time: new Date().toLocaleString() };
        sharedWorker.port.postMessage(data);
      });

      sharedWorker.port.addEventListener("message", (event) => {
        console.log({ event });
        const receivedData = event.data;
        const listItem = document.createElement("li");
        listItem.textContent = `${
          receivedData.source === "page B" ? "发送数据" : "接收数据"
        } ${receivedData.source} ${receivedData.time}`;
        receivedDataListB.appendChild(listItem);
      });
    </script>
  </body>
</html>

worker.js

// worker.js
const clients = [];

onconnect = (event) => {
  const port = event.ports[0];
  clients.push(port);

  port.onmessage = (event) => {
    console.log("Received message:", event.data);
    const data = event.data;
    clients.forEach((client) => {
      client.postMessage(data);
    });
  };

  port.onmessageerror = (error) => {
    console.error(error);
  };

  port.addEventListener("close", () => {
    clients.delete(port);
  });

  port.start();
};

cookie+计时器

⚡通信流程⚡

使用 cookie 和定时器轮询(setInterval)进行前端通信的基本步骤如下:

  1. 设置 Cookie: 在一个页面中,使用 JavaScript 设置需要传递的数据到 cookie 中。可以使用 document.cookie 属性来设置 cookie。例如:

    document.cookie = "data=Hello; expires=Thu, 01 Jan 2026 00:00:00 UTC; path=/";
    
  2. 定时器轮询: 在同一个页面中,使用 setInterval() 函数创建定时器,以便定期检查或发送数据。例如:

    setInterval(function() {
        // 在这里执行轮询的逻辑,可以读取 cookie 中的数据或发送数据到服务器
        var data = document.cookie.replace(/(?:(?:^|.*;\s*)data\s*=\s*([^;]*).*$)|^.*$/, "$1");
        console.log("Data from cookie:", data);
    }, 1000); // 每1秒执行一次轮询
    
  3. 读取 Cookie: 在另一个页面中,也使用 JavaScript 读取 cookie 中的数据。例如:

    var data = document.cookie.replace(/(?:(?:^|.*;\s*)data\s*=\s*([^;]*).*$)|^.*$/, "$1");
    console.log("Data from cookie:", data);
    

🐷缺点🐷

使用 cookie 和定时器轮询(setInterval)进行前端通信虽然简单,但也存在一些缺点:

  1. 安全性问题:

    • Cookie 中存储的数据可以被用户或第三方篡改或窃取,因为它们存储在用户的浏览器中。
    • 如果传输敏感信息,如用户凭证或个人数据,存在泄露的风险。
  2. 性能问题:

    • 定时器轮询可能会增加页面的负载,尤其是当页面存在多个定时器时,会消耗更多的系统资源和网络带宽。
    • 如果定时器频率设置过高,会导致不必要的网络请求和服务器负载,影响用户体验和系统性能。
  3. 跨域限制:

    • 浏览器的安全策略限制了对不同域名下的 cookie 的访问,这意味着跨域通信可能会受到限制或失败
  4. 数据大小限制:

    • 浏览器对于每个 cookie 的大小都有限制,通常在几 KB 到几 MB 之间。如果需要传递大量数据,可能会超出 cookie 的限制,导致通信失败。
  5. 实时性问题:

    • 定时器轮询的实时性受到轮询间隔的影响,不能立即响应数据变化可能存在延迟
  6. 网络流量增加:

    • 即使数据没有变化,定时器轮询仍会发送请求,增加了不必要的网络流量和服务器负载。

综上所述,尽管使用 cookie 和定时器轮询是一种简单的前端通信方法,但也存在诸多安全性、性能和实时性等方面的缺点,需要在实际应用中进行综合考虑和权衡。

🍋示例🍋

//a.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Send and Receive Data</title>
  </head>
  <body>
    <button id="sendDataBtnA">Send Data from A</button>
    <ol id="receivedDatalistA"></ol>
    <script>
      const sendDataBtnA = document.getElementById("sendDataBtnA");
      const receivedDatalistA = document.getElementById("receivedDatalistA");

      sendDataBtnA.addEventListener("click", () => {
        const data = { source: "page A", time: new Date().toLocaleString() };
        document.cookie = `sharedData=${JSON.stringify(data)}`;
      });

      setInterval(() => {
        const sharedData = document.cookie
          .split(";")
          .find((cookie) => cookie.trim().startsWith("sharedData="));
        if (sharedData) {
          const parsedData = JSON.parse(sharedData.split("=")[1]);
          const listItem = document.createElement("li");
          listItem.textContent = `${parsedData.source} - ${parsedData.time}`;
          receivedDatalistA.appendChild(listItem);
          // 清空已获取的数据,避免重复显示
          document.cookie =
            "sharedData=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
        }
      }, 1000); // 每秒钟检查一次 Cookie 中的数据
    </script>
  </body>
</html>

//b.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Send and Receive Data</title>
  </head>
  <body>
    <button id="sendDataBtnB">Send Data from B</button>
    <ol id="receivedDatalistB"></ol>
    <script>
      const sendDataBtnB = document.getElementById("sendDataBtnB");
      const receivedDatalistB = document.getElementById("receivedDatalistB");

      sendDataBtnB.addEventListener("click", () => {
        const data = { source: "page B", time: new Date().toLocaleString() };
        document.cookie = `sharedData=${JSON.stringify(data)}`;
      });

      setInterval(() => {
        const sharedData = document.cookie
          .split(";")
          .find((cookie) => cookie.trim().startsWith("sharedData="));
        if (sharedData) {
          const parsedData = JSON.parse(sharedData.split("=")[1]);
          const listItem = document.createElement("li");
          listItem.textContent = `${parsedData.source} - ${parsedData.time}`;
          receivedDatalistB.appendChild(listItem);
          // 清空已获取的数据,避免重复显示
          document.cookie =
            "sharedData=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
        }
      }, 1000); // 每秒钟检查一次 Cookie 中的数据
    </script>
  </body>
</html>

组件通信(React、Vue3)

React和Vue 3作为两大热门前端框架,React和Vue 3都提供了多种灵活的方式来实现组件间的数据传递和通信。他们的通信方式有相似有差异,我们可以通过比对这两种框架的使用方式,来加深理解。

父传子

ReactVue 3 中,父组件向子组件传递数据的方式类似,主要使用 props 属性。

这里我们不介绍React中的Redux和Mobx和Vue3的pinia和vuex

下面分别介绍在 React 和 Vue 3 中如何进行父传子通信:

React

在 React 中,父组件可以通过 props 将数据传递给子组件。

// 父组件
import React from 'react';
import ChildComponent from './ChildComponent';
​
const ParentComponent = () => {
  const message = 'Hello from parent';
​
  return (
    <div>
      <h1>Parent Component</h1>
      <ChildComponent message={message} />
    </div>
  );
};
​
export default ParentComponent;
​
// 子组件
import React from 'react';
​
const ChildComponent = (props) => {
  return (
    <div>
      <h2>Child Component</h2>
      <p>{props.message}</p>
    </div>
  );
};
​
export default ChildComponent;

在这个例子中,父组件 ParentComponentmessage 作为 prop 传递给子组件 ChildComponent。子组件可以通过 props 对象访问这个数据。

Vue3

在 Vue.js 3 中,父组件也是使用 props 选项将数据传递给子组件。

<!-- 父组件 -->
<template>
  <div>
    <h1>Parent Component</h1>
    <child-component :message="message"></child-component>
  </div>
</template>
​
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
​
const message = ref('Hello from parent');
</script>
​
<!-- 子组件 -->
<template>
  <div>
    <h2>Child Component</h2>
    <p>{{ message }}</p>
  </div>
</template>
​
<script setup>
import { defineProps } from 'vue';
​
const props = defineProps({
  message: {
    type: String,
    required: true
  }
});
</script>

在这个例子中,父组件 ParentComponent 通过 :message="message"message 属性传递给子组件 ChildComponent。子组件使用 defineProps 定义了 message prop,并可以在模板中使用它。

子传父

ReactVue 3 中,子组件向父组件传递数据通常需要通过回调函数事件触发的方式实现。

以下是两者的子传父通信方式的示例:

React

React 中,子组件可以通过调用父组件传递下来的回调函数来向父组件传递数据。

// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
​
const ParentComponent = () => {
  const [message, setMessage] = useState('');
​
  const handleMessageUpdate = (newMessage) => {
    setMessage(newMessage);
  };
​
  return (
    <div>
      <h1>Parent Component</h1>
      <p>Message: {message}</p>
      <ChildComponent onMessageUpdate={handleMessageUpdate} />
    </div>
  );
};
​
export default ParentComponent;
​
// 子组件
import React, { useState } from 'react';
​
const ChildComponent = ({ onMessageUpdate }) => {
  const [inputMessage, setInputMessage] = useState('');
​
  const handleInputChange = (e) => {
    setInputMessage(e.target.value);
  };
​
  const handleSendMessage = () => {
    onMessageUpdate(inputMessage);
    setInputMessage('');
  };
​
  return (
    <div>
      <h2>Child Component</h2>
      <input type="text" value={inputMessage} onChange={handleInputChange} />
      <button onClick={handleSendMessage}>Send Message</button>
    </div>
  );
};
​
export default ChildComponent;

在这个例子中,父组件 ParentComponenthandleMessageUpdate 函数作为 prop 传递给子组件 ChildComponent。子组件在用户输入消息并点击"Send Message"按钮时,会调用 handleMessageUpdate 函数,从而向父组件传递数据。

Vue3

Vue3 中,子组件可以通过 $emit 向父组件发送事件,并传递数据。

<!-- 父组件 -->
<template>
  <div>
    <h1>Parent Component</h1>
    <p>Message: {{ message }}</p>
    <child-component @message-update="updateMessage"></child-component>
  </div>
</template>
​
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
​
const message = ref('');
​
const updateMessage = (newMessage) => {
  message.value = newMessage;
};
</script>
​
<!-- 子组件 -->
<template>
  <div>
    <h2>Child Component</h2>
    <input type="text" v-model="inputMessage" />
    <button @click="sendMessage">Send Message</button>
  </div>
</template>
​
<script setup>
import { ref } from 'vue';
​
const inputMessage = ref('');
​
const emit = defineEmits(['message-update']);
​
const sendMessage = () => {
  emit('message-update', inputMessage.value);
  inputMessage.value = '';
};
</script>

在这个例子中,子组件 ChildComponent 通过 $emit('message-update', inputMessage.value) 向父组件发送 message-update 事件,并传递 inputMessage.value 作为事件参数。父组件通过 @message-update="updateMessage" 监听这个事件,并在 updateMessage 函数中更新 message 的值。

兄弟组件传值

ReactVue 3 中,兄弟组件之间的通信可以通过共享状态或者通过父组件作为中介来实现。

以下是两种方法的示例:

React

通过共享状态(状态提升)

  • 将共享的状态提升到它们的最近公共祖先组件,并通过 props 传递给需要通信的兄弟组件。
  • 兄弟组件通过父组件传递的 props 来共享状态。
  • 这种方式适用于简单的组件通信场景。
// 父组件
import React, { useState } from 'react';
import ChildComponent1 from './ChildComponent1';
import ChildComponent2 from './ChildComponent2';
​
const ParentComponent = () => {
  const [message, setMessage] = useState('');
​
  const handleMessageUpdate = (newMessage) => {
    setMessage(newMessage);
  };
​
  return (
    <div>
      <h1>Parent Component</h1>
      <ChildComponent1 onMessageUpdate={handleMessageUpdate} />
      <ChildComponent2 message={message} />
    </div>
  );
};
​
export default ParentComponent;
​
// 子组件 1
import React, { useState } from 'react';
​
const ChildComponent1 = ({ onMessageUpdate }) => {
  const [inputMessage, setInputMessage] = useState('');
​
  const handleInputChange = (e) => {
    setInputMessage(e.target.value);
  };
​
  const handleSendMessage = () => {
    onMessageUpdate(inputMessage);
    setInputMessage('');
  };
​
  return (
    <div>
      <h2>Child Component 1</h2>
      <input type="text" value={inputMessage} onChange={handleInputChange} />
      <button onClick={handleSendMessage}>Send Message</button>
    </div>
  );
};
​
export default ChildComponent1;
​
// 子组件 2
import React from 'react';
​
const ChildComponent2 = ({ message }) => {
  return (
    <div>
      <h2>Child Component 2</h2>
      <p>Received message: {message}</p>
    </div>
  );
};
​
export default ChildComponent2;

在这个例子中,父组件 ParentComponent 负责在子组件之间传递数据。子组件 ChildComponent1 通过调用父组件传递下来的 handleMessageUpdate 函数来更新父组件的 message 状态。子组件 ChildComponent2 则通过 props 接收父组件的 message 状态。这样,两个子组件就可以间接地进行通信。

Vue3

  • 将需要共享的数据定义在共同的父组件中,然后通过 props 传递给需要通信的兄弟组件。
  • 兄弟组件可以通过 props 接收父组件传递的数据。
  • 这种方式与 React 类似,适用于简单的组件通信场景。
  1. 在父组件中定义共享状态:
<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>Parent Component</h1>
    <child-component1 :message="message" @update-message="updateMessage" />
    <child-component2 :message="message" />
  </div>
</template>
​
<script setup>
import { ref } from 'vue'
import ChildComponent1 from './ChildComponent1.vue'
import ChildComponent2 from './ChildComponent2.vue'const message = ref('')
​
const updateMessage = (newMessage) => {
  message.value = newMessage
}
</script>
  1. 在子组件 1 中更新共享状态:
<!-- ChildComponent1.vue -->
<template>
  <div>
    <h2>Child Component 1</h2>
    <input type="text" v-model="inputMessage" />
    <button @click="sendMessage">Send Message</button>
  </div>
</template>
​
<script setup>
import { ref, defineProps, defineEmits } from 'vue'const props = defineProps({
  message: {
    type: String,
    required: true
  }
})
​
const emits = defineEmits(['update-message'])
​
const inputMessage = ref(props.message)
​
const sendMessage = () => {
  emits('update-message', inputMessage.value)
  inputMessage.value = ''
}
</script>
  1. 在子组件 2 中访问共享状态:
<!-- ChildComponent2.vue -->
<template>
  <div>
    <h2>Child Component 2</h2>
    <p>Received message: {{ message }}</p>
  </div>
</template>
​
<script setup>
import { defineProps } from 'vue'const props = defineProps({
  message: {
    type: String,
    required: true
  }
})
</script>

在这个例子中,我们在父组件 ParentComponent 中定义了一个 message 状态,并将其传递给两个子组件 ChildComponent1ChildComponent2

在子组件 1 ChildComponent1 中,我们定义了一个输入框和一个按钮,当用户输入文本并点击按钮时,我们会通过触发一个自定义事件 update-message 来通知父组件更新 message 状态。

在子组件 2 ChildComponent2 中,我们只需要简单地显示从父组件传递下来的 message 状态即可。

通过这种方式,两个子组件都可以访问和操作父组件中共享的 message 状态,从而实现了兄弟组件之间的数据传递。

跨层级组件通信

在 React 和 Vue 3 中,跨层级组件通信通常涉及到父子组件之间、兄弟组件之间或者祖先组件与后代组件之间的通信。

以下是React 和 Vue 3中常见的方法:

React (Context)

实现步骤:

  • 创建 Context 对象:使用 createContext() 方法创建一个 Context 对象。
  • 在顶层组件中提供 Context 数据:使用 Ctx.Provider 组件来提供 Context 数据。将需要共享的数据作为 value prop 传递给 Ctx.Provider 组件。
  • 在底层组件中使用 Context 数据:在需要访问 Context 数据的组件(比如 B 组件)中,使用 useContext 钩子来获取 Context 数据。将 Ctx 对象作为参数传递给 useContext 钩子,即可访问到共享的数据。

假设我们有以下组件结构:

App
├── Header
│   └── UserInfo
└── Main
    ├── Content
    └── Sidebar

我们希望在 UserInfo 组件中显示用户的名称和头像,而这些数据需要从 App 组件传递下去。这时我们可以使用 Context API 来实现这个需求。

  1. App 组件中创建 Context:
// UserContext.js
import { createContext } from 'react';
​
export const UserContext = createContext();
  1. App 组件中提供 Context 数据:
// App.js
import { UserContext } from './UserContext';
​
function App() {
  const user = {
    name: 'John Doe',
    avatar: 'https://example.com/avatar.jpg',
  };
​
  return (
    <UserContext.Provider value={user}>
      <Header />
      <Main />
    </UserContext.Provider>
  );
}
  1. UserInfo 组件中使用 Context 数据:
// UserInfo.js
import { useContext } from 'react';
import { UserContext } from './UserContext';
​
function UserInfo() {
  const { name, avatar } = useContext(UserContext);
​
  return (
    <div>
      <img src={avatar} alt={name} />
      <span>{name}</span>
    </div>
  );
}

在这个例子中,我们首先在 UserContext.js 文件中创建了一个 UserContext。然后在 App 组件中,我们使用 UserContext.Provider 组件包裹了整个应用程序,并将 user 对象作为 value 传递给它。

UserInfo 组件中,我们使用 useContext 钩子来获取 UserContext 中的数据,并在组件中渲染用户的名称和头像。

这样,我们就实现了从 App 组件到 UserInfo 组件的跨层级数据传递。即使 UserInfo 组件在组件树的中间层级,它也能够直接访问到 App 组件提供的用户数据。

Vue3(Provide 和 Inject)

  • 父组件使用 provide 提供数据,后代组件通过 inject 注入数据,可以在整个组件树中任意深度的组件之间进行通信。
  • 这种方式类似于 ReactContext API,适用于跨层级的组件通信场景。
  1. 创建 Provide 数据

    • 在上层组件(通常是 App 组件)中,使用 provide 选项或 provide 函数来提供共享数据。
    • 可以提供一个值或一个返回值的函数。
    <!-- App.vue -->
    <script setup>
    import { provide } from 'vue';
    ​
    const user = {
      name: 'John Doe',
      email: 'john@example.com',
    };
    ​
    provide('user', user);
    </script>
    ​
    <template>
      <header>...</header>
      <main>
        <child-component />
      </main>
    </template>
    
  2. 使用 Inject 数据

    • 在需要使用 Provide 数据的下层组件中,使用 inject 选项或 inject 函数来注入数据。
    • 可以指定默认值,以防止找不到对应的 Provide 数据。
    <!-- ChildComponent.vue -->
    <script setup>
    import { inject } from 'vue';
    ​
    const user = inject('user', { name: 'Default Name', email: 'default@example.com' });
    </script>
    ​
    <template>
      <div>
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
      </div>
    </template>
    
  3. 多个 Provide

    • 如果需要在应用程序中使用多个 Provide 数据,可以继续使用 provide 选项或函数,并指定不同的键。
    • 在下层组件中,使用对应的键来注入数据。
    <!-- App.vue -->
    <script setup>
    import { provide } from 'vue';
    ​
    const user = {
      name: 'John Doe',
      email: 'john@example.com',
    };
    ​
    const theme = {
      primary: '#007bff',
      secondary: '#6c757d',
    };
    ​
    provide('user', user);
    provide('theme', theme);
    </script>
    ​
    <!-- ChildComponent.vue -->
    <script setup>
    import { inject } from 'vue';
    ​
    const user = inject('user');
    const theme = inject('theme');
    </script>
    

通过这些步骤,你就可以在 Vue 3 应用程序中使用 Provide 和 Inject 实现跨层级组件通信了。上层组件通过 provide 提供数据,下层组件通过 inject 来使用这些数据,无需通过一层层的 props 传递。

差异

差异:

  1. 语法和用法

    • 在 React 中,使用 createContext 创建上下文,然后通过 Context.Provider 提供数据,子组件通过 Context.Consumer 或者 useContext 来接收数据。
    • 在 Vue 3 中,使用 provide 在父组件中提供数据,然后在后代组件中使用 inject 来注入数据。
  2. 数据更新

    • React 中的上下文(Context)是响应式的,当上下文数据发生变化时,使用该上下文的组件会自动更新。
    • Vue 3 中的 provideinject 不是响应式的,即使提供的数据发生变化,不会自动触发后代组件更新,需要手动处理。
  3. 作用域

    • React 的上下文可以在整个组件树中传递和使用,因此适用于跨层级组件通信。
    • Vue 3 中的 provideinject 是基于组件实例的,只能在当前组件及其后代组件中使用,无法在整个应用中全局使用。

相似之处:

  1. 实现跨层级组件通信

    • 无论是 React 的 UserContext 还是 Vue 3 的 provideinject,都是为了解决跨层级组件通信的问题而设计的。
    • 它们都可以让父组件向后代组件传递数据,避免了层层传递 props 的繁琐。
  2. 灵活性

    • 两者都提供了一种灵活的方式来管理和共享数据,使得组件之间的通信更加简洁和高效。

参考文献