跨页面通信

839 阅读4分钟

同源

BroadcastChannel

在相同的源的浏览器上下文(windows,tabs,frames或者iframes)之间进行简单的通信

API

  • 发送方:使用BroadcastChannelIns.postMessage(message)发送
  • 接收方:使用BroadcastChannelIns.addEventListener('message',function)
  1. (on)message: 有数据发送时触发;
  2. (on)onmessageerror: 消息错误时触发;
  3. BroadcastChannelIns.close(): 关闭channel,不再接收;

demo

发送页

// pages/dispather.js
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState } from 'react';

export default function Dispather() {
  const [message, setMessage] = useState('');

  // 发送消息
  const handleSendMessage = () => {
    // new BroadcastChannel的参数发送方和接收方必须是一样的
    const channel = new BroadcastChannel('this_params_must_be_same');
    channel.postMessage(message);
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Dispatcher</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege to be sent:</h4>
        <input
          value={message}
          onChange={e => {
            setMessage(e.target.value);
          }}
        />
        <br />
        <button onClick={handleSendMessage}>click to send</button>
      </main>
    </div>
  );
}

接收页

// pages/receiver.js
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useEffect, useState } from 'react';

export default function Receiver() {
  // --监听消息接收--
  const handleListenMessage = () => {
    // new BroadcastChannel的参数发送方和接收方必须是一样的
    const channel = new BroadcastChannel('this_params_must_be_same');
    console.log('This channel is working:', channel.name, '(channel.name)');
    channel.onmessage = function (event) {
      setMessage(event.data);
    };
  };
  const [message, setMessage] = useState('');

  // handleListenMessage不能直接在组件里执行,因为用的ssr,在server端没有BroadcastChannel这个API
  useEffect(() => {
    handleListenMessage();
  }, []);
  // ReferenceError: BroadcastChannel is not defined
  // handleListenMessage();
  return (
    <div className={styles.container}>
      <Head>
        <title>Receiver</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege received:</h4>
        <h4>{message}</h4>
        <br />
      </main>
    </div>
  );
}

Shared Worker

普通的 Worker 之间独立运行、数据互不相通;而多个 Tab 注册的 Shared Worker 则可以实现数据共享。

API

  • 发送方:使用SharedWorkerIns.port.postMessage(message)发送
  • 接收方:使用SharedWorkerIns.port.addEventListener('message',function)

demo

发送页

import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';

export default function Dispather() {
  const [message, setMessage] = useState('');
  const [worker, setWorker] = useState('');
  useEffect(() => {
    // new SharedWorker引用同一个脚本,名字必须要一样才能数据共享
    setWorker(new SharedWorker('/worker.js', 'this_name_must_be_same'));
  }, []);

  // 发送消息
  const handleSendMessage = () => {
    worker.port.postMessage(message);
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Dispatcher</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege to be sent:</h4>
        <input
          value={message}
          onChange={e => {
            setMessage(e.target.value);
          }}
        />
        <br />
        <button onClick={handleSendMessage}>click to send</button>
      </main>
    </div>
  );
}

接收页

import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useEffect, useState } from 'react';

export default function Receiver() {
  const [worker, setWorker] = useState('');
  useEffect(() => {
    setWorker(new SharedWorker('/worker.js', 'this_name_must_be_same'));
    handleListenMessage();
  }, []);
  // --监听消息接收--
  const handleListenMessage = () => {
    // new SharedWorker引用同一个worker
    console.log('This worker is working:', worker);
    if (worker.port) {
      worker.port.onmessage = function (event) {
        setMessage(event.data);
      };
    }
  };
  const [message, setMessage] = useState('');

  return (
    <div className={styles.container}>
      <Head>
        <title>Receiver</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege received:</h4>
        <h4>{message}</h4>
        <br />
      </main>
    </div>
  );
}

worker

// public/worker.js
// worker.js
const ports = new Set();
// 链接时触发
self.onconnect = event => {
  // 通过 event.ports 拿到 父线程MessageEvent,new SharedWorker时第二个参数想通被认为是同一个port
  const port = event.ports[0];
  console.log(
    '子线程worker的connect事件在父线程port的new SharedWorker时候触发,此时的port是:',
    event.ports[0],
    '一共的父线程port有:',
    ports
  );

  ports.add(port);

  port.onmessage = e => {
    // port.onmessage 监听父线程的消息
    console.log(e.data);
    console.log(
      '父线程port的message事件在postMessage时候触发,此时的port是:',
      port,
      'message是:',
      e.data,
      '一共的父线程port有:',
      ports,
      '排除自己还有',
      Array.from(ports).filter(p => p !== port)
    );
    Array.from(ports)
      .filter(p => p !== port) // 广播消息时把自己除掉
      .forEach(p => p.postMessage(e.data)); // 向链接池里的用户发送消息
  };
};

LocalStorage

  • 同源限制:一个源共享同一个 localStorage,a.meituan.com 和 b.meituan.com 是两个域名,不能共享storage;但同源不同tab页可以共享localStorage
  • 大小限制:localStorage的最大限制是5MB,如果存满了,再往里存东西,或者要存的东西超过了剩余容量,会存不进去并报错(QuotaExceededError 22)

API

  • 发送方:window.localStorage.setItem('key', JSON.stringify(value));

  • 接收方:window.addEventListener('storage'; function)

只有当value真正改变才会触发事件storage

demo

发送页

// pages/dispather.js
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';

export default function Dispather() {
  const [message, setMessage] = useState('');

  // 发送消息
  const handleSendMessage = () => {
    window.localStorage.setItem('localStore', JSON.stringify(message));
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Dispatcher</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege to be sent:</h4>
        <input
          value={message}
          onChange={e => {
            setMessage(e.target.value);
          }}
        />
        <br />
        <button onClick={handleSendMessage}>click to send</button>
      </main>
    </div>
  );
}

接受页

import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useEffect, useState } from 'react';

export default function Receiver() {
  useEffect(() => {
    handleListenMessage();
  }, []);
  // --监听消息接收--
  const handleListenMessage = () => {
    window.onstorage = function (e) {
      console.log(e);
      if (e.key === 'localStore') {
        const data = JSON.parse(e.newValue);
        setMessage(data);
      }
    };
  };
  const [message, setMessage] = useState('');

  return (
    <div className={styles.container}>
      <Head>
        <title>Receiver</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege received:</h4>
        <h4>{message}</h4>
        <br />
      </main>
    </div>
  );
}

非同源

iframe

API

  • 发送方:使用otherWindow.postMessage(message, targetOrigin, [transfer]);
  • 接收方: window.addEventListener('message', function)
  1. otherWindow:父页面使用dom.contentWindow/ [iframename]获取子页面window的引用;子页面使用e.source/ window.top/ window.parent 获取父页面window的引用
  2. 在非同源的情况下,不能访问其他windo的dom,所以不能直接访问window,但可以调用方法
  3. targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI
  4. 不希望从其他网站接收message,不要为message添加事件侦听即可。希望从其他网站接收message,需要用origin和source属性验证发件人的身份

demo

发送页面

// localhost:3000/dispather
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';

export default function Dispather() {
  const [message, setMessage] = useState('');
  const handleCallChildFun = () => {
    // 获取子页面
    // const iFrame = document.getElementById('iframe');
    // iFrame.contentWindow.postMessage(message, 'http://localhost:3001/receiver');
    console.log('父页面调用子页面的postmessage方法,传递消息:', message);
    const iFrameWindow = iframewindow;
    iFrameWindow.postMessage(message, 'http://localhost:3001/receiver');
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Dispatcher</title>
      </Head>
      <main className={styles.main}>
        <h4>This is messege to be sent:</h4>
        <input
          value={message}
          onChange={e => {
            setMessage(e.target.value);
          }}
        />
        <br />
        <button onClick={handleCallChildFun}>将父页面的消息传递</button>
        <br />
        {/* <iframe id="iframe" style={{ width: '80vw', height: '60vh' }} src="http://localhost:3001/receiver" /> */}
        <iframe name="iframewindow" style={{ width: '80vw', height: '60vh' }} src="http://localhost:3001/receiver" />
      </main>
    </div>
  );
}

接收页面

// localhost:3000/receiver
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useEffect, useState } from 'react';

export default function Receiver() {
  useEffect(() => {
    // 子页面监听接收消息
    window.addEventListener(
      'message',
      e => {
        /**
        eventInit : {
          data	Object             消息的具体内容
          origin	String     消息的域,在这里是父页面
          lastEventId	String
          channel	String
          source	Object     消息的来源,这里是父页面window对象(window.parent/window.top)
          ports	Array<MessagePort>
        }
        */
        console.log('子页面接受到消息:', e.data, e);
        if (e.origin === 'http://localhost:3000') {
          console.log(window.top);
          setMessage(e.data);
        }
      },
      false
    );
  }, []);
  const [message, setMessage] = useState('');

  return (
    <div className={styles.container}>
      <Head>
        <title>Receiver</title>
      </Head>

      <main className={styles.main}>
        <h4>This is messege received:</h4>
        <h4>{message}</h4>
        <br />
      </main>
    </div>
  );
}