前端跨页面通讯终极指南⑥: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是一个能被同源多页面共享的后台线程,它通过"单一实例+管理多端口"的模式,实现跨页面通信与数据协同。

相关推荐
_AaronWong1 天前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode1 天前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户5433081441941 天前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo1 天前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭1 天前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木1 天前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮1 天前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati1 天前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉1 天前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain
wuhen_n1 天前
双端 Diff 算法详解
前端·javascript·vue.js