SharedWorker 讲解 & 广播实现

1,802 阅读5分钟

本文示例:www.risu-p.com/planet/#/sh…

参考 MDN: developer.mozilla.org/zh-CN/docs/…

SharedWorker 代表一种特定类型的 Worker ,可以从多个浏览器上下文中访问,例如多个窗口、iframe 或其它 worker。它实现了一个不同于普通 Worker 的接口,具有不同的全局作用域 SharedWorkerGlobalScope (en-US) 

注意:如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及 端口)

一、与普通 Worker 的不同

  1. 普通 Worker每次 new 都会创建一个新的 worker线程,而 SharedWorker 对于相同 url 脚本只会创建一个共享线程。每个与 SharedWorker 连接的主线程,在SharedWorker 中都会有一个 port 对象(稍后讲解)与之对应

  2. SharedWorker 通过 .port 来发送、接收消息

二、SharedWorker 具有的属性

2.1 .onerror

一个 EventListner ,当 error 类型的事件冒泡到 worker 时触发

2.2 .port

一个 MessagePort 对象,该对象用来与 shared worker 通信、控制

三、MessagePort 对象

在 主线程 和 shared worker线程 中,双方都是通过 MessagePort 对象进行通信的  

主线程中:通过 new SharedWorker() 返回对象的 .port 属性即可拿到 MessagePort 对象

worker线程中:通过 onconnect 事件对象 event.ports[0] 拿到对应的 MessagePort 对象

3.1 方法

3.1.1 MessagePort.postMessage

从端口发送一条消息,并且可选是否将对象的所有权交给其他浏览器上下文

对象所有权:

worker线程与主线程之间的数据通信是值传递,需要先串行化成字符串,再还原

如果传500M的二进制对象,生成原文件的拷贝显然不现实

所以可以让主线程直接将对象所有权交给对方,这样worker线程就快速有了对该二进制数据的操作权;同时主线程也会失去对该数据的操作权,防止出现同时修改数据的局面

3.1.2 MessagePort.start

启动发送该端口的消息队列;在未调用之前,发往该端口的消息都在队列中等待

简而言之就是:

主线程不调用port.start(),就没法接受到worker线程发来的消息;但还是可以向worker线程发送消息的

只有用 port.addEventListenr('message') 时需要手动显式地调用该方法

若使用 port.onmessage 会默认隐式调用 port.start()

主线程、worker线程中都需要调用:

worker.port.addEventListener("message", (event) => {
  console.log(`接收到了消息:${event.data}`);
});
worker.port.start();

# 等同于

worker.port.onmessage = (event) => {
  console.log(`接收到了消息:${event.data}`);
};

3.1.3 MessagePort.close

断开端口连接,它将不再是激活状态

3.2 事件回调

3.2.1 MessagePort.onmessage

收到消息时触发

3.2.2 MessagePort.onmessageerror

端口收到的消息无法被反序列化

四、示例

4.1 基本使用

该基本示例来自 MDN

在这个 shared worker 例子中 (运行 shared worker), 我们有两个 HTML 页面。每个页面都用了同一个 shared worker 执行计算,即使在不同窗口内运行

下面的代码展示了如何通过 SharedWorker() 方法来创建一个共享进程对象:

var myWorker = new SharedWorker("worker.js");

这会触发 worker 线程的 connect 事件,worker 线程在connect 事件中启动与主线程关联的端口后,即可接受主线程发来的消息

 

主线程通过 MessagePort 对象与 worker 通信,这个对象由SharedWorker.port (en-US) 属性获得:

/* 主线程 向 shared worker 发送消息 */
first.onchange = function() {
  myWorker.port.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}
second.onchange = function() {
  myWorker.port.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}


/* 主线程 接收来自 shared worker 的消息 */
myWorker.port.onmessage = function(e) {
  result1.textContent = e.data;
  console.log('Message received from worker');
}

在 worker 中我们需要使用 SharedWorkerGlobalScope.onconnect (en-US) 处理 主线程 连接到 worker线程 的事件

可以在 connect 事件 event.ports 属性中获取到 主线程 与 worker 关联的端口对象(每个主线程对应一个新的 port

然后我们使用 port.start() 方法开始接收该端口的消息队列,并通过监听 onmessage 事件处理来自主线程的消息:

onconnect = function(e) {
    var port = e.ports[0];


    port.addEventListener('message', function(e) {
      var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
      port.postMessage(workerResult);
    });


    /* 开始发送该端口中的消息队列,只有用`port.addEventListenr('message')`时需要手动显式调用 */
    /* 使用`port.onmessage`会默认隐式调用`port.start()` */
    port.start();
}

 

下个例子是更详细的运用:

4.2 向各个页面广播

在基本示例中,2个页面只是单独与work线程通信。本例中,我们将实现 worker线程 向所有连接了的主线程广播消息

原理:worker 中存下与每个主线程关联的 port 。广播消息时通过遍历,以向所有主线程发送数据。由于自己维护了一个 portPool 数组,断开连接时,也需要通知 worker 删除数组中对应的 port

sharedWorker.js:

/* 存下与 shared worker 连接了的所有端口 */
const portPool = [];


/* 连接事件触发 */
self.onconnect = function (e) {
  console.log("worker内:连接事件触发");


  const port = e.ports[0];
  // 将 port 添加到 portPool 中
  portPool.push(port);


  port.onmessage = (e) => {
    console.log(`worker 接收到了消息:${e.data}`);
    if (e.data === "Close") {
      // 关闭连接,移除对应 port
      const index = portPool.findIndex((p) => p === port);
      portPool.splice(index, 1);
      port.close();
      // 广播,连接数量
      boardcast(portPool.length);
    }
  };


  // 广播,连接数量
  boardcast(portPool.length);
};


/* 向当前所有连接了的 port 广播消息 */
function boardcast(message) {
  portPool.forEach((port) => {
    port.postMessage(message);
  });
}

这样我们就实现了向多个页面广播消息的功能

在页面中使用:

const SharedWorkerPage: FC<IProps> = memo(({}) => {
  const workerRef = useRef<SharedWorker | null>(null);


  const [connectCount, setConnectCount] = useState<number>(0);


  /* 通知断开与 shared worker 的连接 */
  const closeSharedWorkerPort = useCallback(() => {
    if (workerRef.current) {
      workerRef.current.port.postMessage("Close");
      workerRef.current = null;
    }
  }, []);


  /* 初始化 */
  useEffect(() => {
    const worker = new SharedWorker(
      `${window.location.origin}${window.location.pathname}sharedWorker/sharedWorker.js`
    );
    workerRef.current = worker;
    worker.port.postMessage("Hello");
    worker.port.onmessage = (event) => {
      console.log(`主线程 接收到了 worker 消息:${event.data}`);
      // 接受一个当前有多少个页面连接了 shared worker 的个数
      const connectCount = event.data;
      setConnectCount(connectCount);
    };


    /**
     * 正常情况下,页面关闭,即使不手动调`port.close()`,`shared worker`也会自动关闭与该页面的连接
     * 当没有页面与`shared worker`连接时,就`worker`自动删除了
     *
     * 但由于这里我们自己在`worker`中维护了 portPool 数组,来记录当前有多少个连接
     *
     * 若直接改地址栏url,导致组件没有走卸载生命周期
     * 此时`shared worker`没有收到"Close"消息,就不会将与该页面对应的port移除portPool数组,导致记录的连接数出错
     * 故再监听window "beforeunload",以发送"Close"消息
     */
    window.addEventListener("beforeunload", closeSharedWorkerPort);


    return () => {
      /* 组件卸载时,通知关闭与 shared worker 的连接 */
      closeSharedWorkerPort();
      window.removeEventListener("beforeunload", closeSharedWorkerPort);
    };
  }, []);


  return (
    <div className={PREFIX}>
      当前有 {connectCount} 页面连接到了 shared worker
    </div>
  );
});

 

七、调试 shared worker

shared workerconsole.log 输出的信息,在页面的控制台中无法看到

 

对于chrome,在浏览器地址栏输入 chrome://inspect

点击 Shared workers > inspect

转存失败,建议直接上传图片文件

即可看到 shared workerconsole.log 输出:

转存失败,建议直接上传图片文件