前端跨页面通讯终极指南⑥:SharedWorker 用法全解析

前言

前面的文章已经介绍了postMessagelocalStoragemessageChannelbroadcastChannel以及window.name。今天要介绍一种"多页面协同"场景的工具------SharedWorker

不同于普通Worker只能被单个页面独占,SharedWorker能被同一域名下的多个页面共享 ,实现高效的"多页面数据中枢"。本文就带你了解SharedWorker跨页面通讯的核心用法。

1. 什么是SharedWorker?

在介绍SharedWorker之前,我们先回顾下Worker的基本概念:Worker是HTML5引入的后台线程机制,能让JavaScript在主线程之外运行,避免复杂计算阻塞页面渲染。而SharedWorker,顾名思义,是"可共享的Worker",它有两个核心特点:

  • 跨页面共享:同一域名下的多个标签页、iframe,甚至不同窗口,都能连接到同一个SharedWorker实例,实现数据互通。
  • 独立线程:运行在独立于所有页面主线程的后台线程中,既不会阻塞页面,也能统一处理多页面的请求。
  • 域名隔离:遵循同源策略,只有相同协议、域名、端口的页面,才能共享同一个SharedWorker。

简单来说,SharedWorker就像一个"公共服务端",多个页面作为"客户端"与之建立连接,通过它完成数据的传递与协同。和普通Worker的"一对一"模式不同,它是"一对多"的通讯方案。

2. SharedWorker如何实现跨页面通讯?

SharedWorker的通讯原理可以概括为"单一实例+多端口连接",先看一张图:

具体流程是:

  1. 创建实例 :每个页面通过new SharedWorker('./share.js')创建实例时,浏览器会启动一个SharedWorker后台线程,加载指定的脚本文件,这个脚本文件是共享。
  2. 端口建立:每个页面连接到SharedWorker后,都会与Worker建立一个独立的"消息端口"(MessagePort),这是页面与Worker之间的通讯通道。
  3. 数据传递 :页面和Worker都是通过通过端口发送消息port.postMessage,都是通过port.onmessage接收数据进行处理,再通过端口将结果反馈给单个页面,或广播给所有连接的页面。
  4. 实例销毁:当所有连接到SharedWorker的页面都关闭时,Worker后台线程才会被浏览器销毁,释放资源。

说了这么多,接下来我们进行实践操作。

3. 实践案例

SharedWorker的使用分为"Worker脚本"和"页面脚本"两部分,Worker脚本负责核心逻辑,页面脚本负责建立连接和收发消息。

实现需求:同一域名下的pageA和pageB页面,通过SharedWorker实现消息互发,且任意页面可以通过Worker广播消息给所有页面。

可以先看一张页面如何连接到Worker流程图:

页面端我们只需要接收发送消息即可,Worker端我们需要收集注册的端口并进行逻辑处理,下面我们一步步来实现:

3.1 步骤1:编写SharedWorker核心脚本(share.js)

share.js的核心逻辑:是使用Map结构,通过connect将所有连接Worker的页面进行收集端口,每个页面需要唯一的pageId用于后续私发消息。

页面发送数据分为三种,进入页面需要注册到share.js,发送广播消息,发送私信需要target页面的pageId。 数据如下:

类型 用途 参数
register 页面注册 pageId
broadcast 广播消息 pageId, data
private 点对点消息 pageId, target, data
js 复制代码
// 注册页面
port.postMessage({                                              
    type: 'register',                                           
    pageId: 'page-123'                                          
});                                                            
// 发送广播                                                                     
port.postMessage({                               
   type: 'broadcast',                                          
   pageId: 'page-123',                                         
   data: 'Hello everyone!'                                     
});                                                             
// 发送私信                                                                     
port.postMessage({                               
   type: 'private',                                            
   pageId: 'page-123',                                         
   target: 'page-456',                                         
   data: 'Hi page-456!'                                        
}); 

share.js脚本:

js 复制代码
// 存储所有页面与Worker的连接端口(使用 Map,key 是 pageId,value 是 port)
const connections = new Map();

console.log('打印***self', self)
// 监听新页面的连接请求
self.addEventListener('connect', (e) => {
    // 获取当前页面的通讯端口
    const port = e.ports[0];

    // 注意:此时还不知道 pageId,需要等待 register 消息
    console.log(`新页面尝试连接,等待注册...`);

    // 允许端口通讯
    port.start();

    // 向页面发送连接成功消息(用于调试)
    port.postMessage({
        from: 'Worker',
        type: 'connected',
        data: `Worker 已连接,当前连接数:${connections.size}`
    });

    // 监听端口的消息事件(接收页面发来的消息)
    port.onmessage = (msg) => {
        console.log('Worker收到消息:', msg.data);
        const { type, target, data, pageId } = msg.data;

        // 0. 处理注册消息(页面连接时立即发送)
        if (type === 'register') {
            // 如果该 pageId 已存在,说明是页面刷新,先关闭旧连接
            if (connections.has(pageId)) {
                const oldPort = connections.get(pageId);
                console.log(`页面 ${pageId} 刷新,清理旧连接`);
                try {
                    oldPort.close();
                } catch (e) {
                    // 忽略关闭错误
                }
            }

            // 保存新连接
            port.pageId = pageId;
            connections.set(pageId, port);
            console.log(`页面 ${pageId} 已注册,当前在线:[${Array.from(connections.keys()).join(', ')}]`,connections);
            return;
        }

        // 保存页面标识到端口对象(兼容旧的发送方式)
        if (pageId && !port.pageId) {
            port.pageId = pageId;
            connections.set(pageId, port);
            console.log(`页面 ${pageId} 已注册(兼容模式)`);
        }

        // 1. 广播消息:发送给所有连接的页面
        if (type === 'broadcast') {
            console.log(`广播消息给 ${connections.size} 个连接`);
            connections.forEach((conn, id) => {
                try {
                    conn.postMessage({ from: 'Worker', data: `广播消息:${data}` });
                } catch (e) {
                    console.log(`发送给 ${id} 失败,移除连接`);
                    connections.delete(id);
                }
            });
        }
        // 2. 点对点消息:仅发送给目标页面(这里用页面标识区分)
        else if (type === 'private') {
            console.log(`私发消息给 ${target},当前连接:[${Array.from(connections.keys()).join(', ')}]`);

            if (connections.has(target)) {
                try {
                    connections.get(target).postMessage({ from: 'Worker', data: `私发消息:${data}` });
                } catch (e) {
                    console.log(`发送给 ${target} 失败,移除连接`);
                    connections.delete(target);
                }
            } else {
                console.log(`警告:未找到目标页面 ${target}`);
            }
        }
    };

    // 监听端口关闭事件(页面关闭或主动断开)
    port.addEventListener('close', () => {
        // 通过 pageId 删除连接
        if (port.pageId) {
            connections.delete(port.pageId);
            console.log(`页面 ${port.pageId} 断开连接,当前在线:[${Array.from(connections.keys()).join(', ')}]`);
        }
    });

    // 允许端口通讯(必须调用,否则无法收发消息)
    port.start();
});

3.2 步骤二:编写页面脚本(pageA.html 和 pageB.html)

代码逻辑如下:唯一需要区别的是pageId,页面B只需将pageId改为page-456,并调整"发给页面B"按钮的逻辑为"发给页面A"即可。

页面的创建流程如下:

js 复制代码
1. 页面创建 SharedWorker 实例
   ↓
2. 调用 port.start() 启动通信
   ↓
3. 注册 onmessage 监听器
   ↓
4. 发送 register 消息(携带 pageId)
   ↓
5. Worker 保存连接到 Map: connections.set(pageId, port)
   ↓
6. 页面可以开始发送/接收消息

代码如下:

js 复制代码
<body>
    <h3>页面A(标识:page-123)</h3>
    <input type="text" id="msgInput" placeholder="输入消息">
    <button onclick="sendBroadcast()">广播消息</button>
    <button onclick="sendToPageB()">发给页面B</button>
    <div id="log"></div>

    <script>
        // 页面唯一标识
        const pageId = 'page-123';
        // 1. 创建SharedWorker实例,指定Worker脚本路径
        const worker = new SharedWorker('./share.js');
        // 2. 获取与Worker的通讯端口
        const port = worker.port;

        // 3. 允许端口通讯(必须在监听器之前调用)
        port.start();

        // 4. 监听Worker发来的消息(使用onmessage更可靠)
        port.onmessage = (msg) => {
            console.log('页面A收到消息:', msg.data);
            const log = document.getElementById('log');
            log.innerHTML += `<p>收到:${JSON.stringify(msg.data)}</p>`;
        };

        // 5. 连接成功后立即发送注册消息
        port.postMessage({
            type: 'register',
            pageId: pageId
        });

        // 发送广播消息
        function sendBroadcast() {
            const input = document.getElementById('msgInput');
            port.postMessage({
                type: 'broadcast',
                pageId: pageId,
                data: input.value
            });
        }

        // 发送点对点消息给页面B(页面B标识为page-456)
        function sendToPageB() {
            const input = document.getElementById('msgInput');
            port.postMessage({
                type: 'private',
                pageId: pageId,
                target: 'page-456',
                data: input.value
            });
        }

        // 页面关闭时断开连接
        window.addEventListener('beforeunload', () => {
            port.close();
        });
    </script>
</body>

3.3 实际调试

3.3.1 如何调试share.js

通过Chrome如何查看share.js中打印的数据,页面是无法访问的,因为它是独立的控制台。

为什么控制台要独立?这是因为SharedWorker运行在与页面主线程完全隔离的独立线程中,从浏览器架构和安全设计出发,其控制台输出也需与页面线程分离,避免线程间的信息干扰。

打开步骤:

  1. 在Chrome浏览器中直接访问地址:chrome://inspect/#workers
  2. 页面会列出当前浏览器中所有运行的Worker实例,找到目标SharedWorker对应的"inspect"链接并点击,即可打开专属控制台。

控制台打印:

3.3.2 页面发送广播或私发消息

  1. 将share.js、pageA.html、pageB.html部署到同一域名下(本地可用live-server启动)。
  2. 同时打开两个页面,在页面A输入消息并点击"广播消息",两个页面都会收到Worker转发的消息。
  3. 点击"发给页面B",只有页面B会收到消息,实现点对点通讯。

页面关闭,自动销毁

4、SharedWorker的注意事项

4.1 同源策略限制严格

SharedWorker的同源限制比普通Worker更严格:协议、域名、端口必须完全一致,即使是子域名(如a.example.com和b.example.com)也无法共享。

4.2 必须部署才能运行,本地直接打开无效

由于浏览器的安全限制,直接双击本地HTML文件(file://协议)创建SharedWorker会报错。必须通过HTTP/HTTPS协议部署,本地可使用live-server、http-server等工具启动服务。

4.3 端口通讯需"双向启动"

页面端和Worker端的port.start()必须都调用,否则无法正常收发消息。尤其是在使用addEventListener绑定消息事件时,这一步不能省略(若用onmessage属性绑定,部分浏览器会自动启动,但建议统一调用start())。

4.4 消息数据需可序列化

通过port传递的消息数据,必须是可结构化克隆的类型(如对象、数组、字符串等),不能传递函数、DOM元素等不可序列化的数据。若需传递复杂数据,可先转为JSON字符串,接收后再解析。

5. 总结

最后总结一下:SharedWorker是一个能被同源多页面共享的后台线程,它通过"单一实例+管理多端口"的模式,实现跨页面通信与数据协同。

相关推荐
PineappleCoder7 小时前
还在重复下载资源?HTTP 缓存让二次访问 “零请求”,用户体验翻倍
前端·性能优化
拉不动的猪7 小时前
webpack编译中为什么不建议load替换ast中节点删除consolg.log
前端·javascript·webpack
李姆斯7 小时前
Agent时代下,ToB前端的UI和交互会往哪走?
前端·agent·交互设计
源码获取_wx:Fegn08957 小时前
基于springboot + vue健身房管理系统
java·开发语言·前端·vue.js·spring boot·后端·spring
闲谈共视7 小时前
基于去中心化社交与AI智能服务的Web钱包商业开发的可行性
前端·人工智能·去中心化·区块链
CreasyChan7 小时前
C# 反射详解
开发语言·前端·windows·unity·c#·游戏开发
JIngJaneIL8 小时前
基于Java+ vue智慧医药系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
hashiqimiya9 小时前
两个步骤,打包war,tomcat使用war包
java·服务器·前端
零度@10 小时前
Java中Map的多种用法
java·前端·python