前端 WebSocket 实战:从基础到进阶

什么是webSocket

官方介绍: WebSocket 是一种基于 TCP 协议的全双工通信协议,允许客户端和服务器之间进行实时、双向的数据传输。与传统的 HTTP 请求-响应模式不同,WebSocket 在建立连接后可以持续保持通信状态,从而实现高效的数据交互。
前端可以这样理解 普通的ajax请求我们总是一个post或者get发送一个后端接口api 如:/api/getUserInfo 并且获取到返回值 result。这个请求是一次性的发送并拿到结果即流程终结。如果想要不定时的获取后台数据来在页面进行呈现。那就需要后端可以一直给我们最新的数据。webSocket技术就可以。即我发送一个请求后,后端会源源不断的给我数据。不需我再关心获取数据都时机了,只要你给我数据 我就触发我的回调函数来更新页面。以及我们也可以与后端实施通信。

实用场景

IM实时聊天
页面需要实时更新变动
实时位置更新
多人在线游戏
语音与视频通话(webRTC更好)

webSocket的主要用法

js 复制代码
// 创建 WebSocket 连接 
const socket = new WebSocket('ws://abc.com/socket'); 
// 监听连接打开事件 
socket.addEventListener('open', () => {
    console.log('WebSocket 连接已建立');
    // 发送消息给服务器 
    socket.send(JSON.stringify({
        type: 'greeting', message: '你好,我是测试数据' 
    }));
});
// 监听消息事件 
socket.addEventListener('message', (event) => { 
    console.log('收到服务器消息:', event.data); 
}); 
// 监听错误事件 
socket.addEventListener('error', (error) => {
    console.error('WebSocket 错误:', error);
}); 
// 监听连接关闭事件 
socket.addEventListener('close', (event) => { 
    console.log('WebSocket 连接已关闭:', event.code, event.reason);
});

webSokcet常见的错误与解决办法

1、心跳相关

我们与后端建立链接后,经常因为用户的网络环境等导致链接断线,或者说前端如何能够知道后端服务是否关闭了等都需要一个机制来保障,这个机制就是心跳链接机制,我们定时给服务器发送消息(以下简称ping),服务器收到我们发送的消息后在回复我们一个消息(以下简称pong);如此重复的进行ping和pong(简化记忆:如同打乒乓球一样)。只要信息不断我们就认为目前的状态是连接中。

让我们实现一下

js 复制代码
let heartbeatInterval;

socket.addEventListener('open', () => {
  console.log('WebSocket 连接已建立');
  // 每隔 30 秒发送一次心跳
  heartbeatInterval = setInterval(() => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'heartbeat' }));
    }
  }, 30000);
});

socket.addEventListener('close', () => {
  console.log('WebSocket 连接已关闭');
  clearInterval(heartbeatInterval); // 清除定时器
});

看上去很完美,实际运行起来关注一段时间,发现也确实没啥问题。但是真的就如此美丽?没啥坑?再次分析:

  1. 用到了定时器,是官方api应该没啥bug
  2. 调用了webSocket,最基础的api也是不会出问题的
  3. 本身也就如此简单的代码,让大模型分析都得说没问题

其实是有问题的

问题出现在定时器上,具体可以点击查看具体说明

从 Chrome 88 开始,系统会对链接的 JS 计时器施加严格的节流限制

简单来说就是,因为浏览器的优化原因,setTimeout()和setInterval(),在浏览器窗口非激活的状态下会停止工作或者以极慢的速度工作,因此当用户最小化窗口、切到别的页签、切换应用后等会导致定时器不准,即我们的心跳ping发送的不及时,后端会认为我们 掉线 了。如何解决呢?

webWork

Web Worker 为什么可以解决这个问题?

Web Worker 是运行在后台线程中的 JavaScript 代码,与主线程分离。

  1. 独立线程
    Web Worker 运行在一个独立的线程中,不受主线程状态的影响。即使页面被最小化或切换到其他标签页,Web Worker 仍然可以在后台正常运行。
  2. 不依赖页面可见性
    Web Worker 的生命周期与页面的可见性无关。只要 Worker 没有被显式终止,它将继续运行,无论页面是否处于激活状态。
  3. 定时器在 Worker 中的行为
    在 Web Worker 中使用的 setTimeout()setInterval() 不会受到页面非激活状态的限制。它们的行为与页面激活时一致,不会被浏览器优化策略干扰。

既然webWork中的定时器不受影响 那我们来用webWork来封装自己的定时器anIntervalanTimeout

js 复制代码
 // 增强型定时器管理模块
export const anInterval = (function () {
  let worker;
  const listeners = new Map();
  let idCounter = 0; // 用于生成唯一ID

  function createWorker() {
    const script = `
      const intervals = new Map();
      onmessage = function(event) {
        const { command, id, time } = event.data;
        
        if (command === 'clear') {
          clearTimeout(intervals.get(id));
          intervals.delete(id);
          return;
        }

        if (intervals.has(id)) {
          clearTimeout(intervals.get(id));
        }
        
        function anIntervalF() {
          console.log('id',id);
          postMessage({ id });
          intervals.set(id, setTimeout(anIntervalF, time));
        }
        // 使用 setTimeout 延迟第一次执行
        intervals.set(id, setTimeout(anIntervalF, time));
      }
    `;
    const blob = new Blob([script], { type: 'text/javascript' });
    const url = URL.createObjectURL(blob);
    worker = new Worker(url);

    worker.addEventListener('message', function (e) {
      const { id } = e.data;
      if (listeners.has(id)) {
        listeners.get(id).forEach(fn => fn());
      }
    });
    console.log('Worker 创建成功');
  }

  function on(fn, time) {
    if (!worker) {
      createWorker();
    }
    // 使用字符串ID代替Symbol
    const id = 'timer_' + (idCounter++);
    if (!listeners.has(id)) {
      listeners.set(id, []);
    }
    listeners.get(id).push(fn);
    worker.postMessage({ id, time });
    // console.log('定时器设置成功', id, time);
    return id;
  }

  function off(id) {
    if (listeners.has(id)) {
      listeners.delete(id);
      worker?.postMessage({ command: 'clear', id });
      // console.log('定时器清除成功', id);
    }
  }

  return { on, off };
})();

anInterval 是一个使用 Web Workers 实现的自定义间隔函数。它返回一个包含 onoff 方法的对象,用于设置和清除定时器。

方法 on(fn, time)

  • 参数
    • fn (Function): 定时器触发时要执行的回调函数。
    • time (Number): 定时器的时间间隔,单位为毫秒。
  • 返回值
    • (String): 定时器的唯一标识符,用于后续清除定时器。
  • 用法
  • on(fn, time) 检查 Worker 是否存在,如果不存在则创建 Worker。 生成一个唯一的定时器标识符。 将标识符和回调函数存储在 listeners Map 中。 向 Worker 发送消息,传递标识符和时间间隔。
  • off(id) 检查 listeners Map 中是否存在该标识符。 如果存在,则删除该标识符及其对应的回调函数。 向 Worker 发送消息,指示清除定时器。

使用方法

javascript 复制代码
// 创建一个定时器,每隔1秒触发一次
let id = anInterval.on(() => {
  console.log("每秒触发一次");
}, 1000);

// 5秒后清除定时器
setTimeout(() => {
  anInterval.off(id);
  console.log("定时器已清除");
}, 5000);

最新的心跳就可用anInterval来代替window中的setInterval了 代码如下:

js 复制代码
let heartbeatInterval;

socket.addEventListener('open', () => {
  console.log('WebSocket 连接已建立');
  // 每隔 30 秒发送一次心跳
  heartbeatInterval = anInterval.on(() => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'heartbeat' }));
    }
  }, 30000);
});

socket.addEventListener('close', () => {
  console.log('WebSocket 连接已关闭');
  anInterval.off(heartbeatInterval); // 清除定时器
});

代替webSocket的方法

是否有其他更好的方法呢 ,在不考虑非常及时的情况下。我们可以用普通的轮询来进行请求接口获取数据。或者用 SSE (Server-Sent Events)

后续考虑将webSocket封装一下省去每次都要处理心跳和浏览器休眠等问题

相关推荐
顾林海17 分钟前
Flutter 图标和按钮组件
android·开发语言·前端·flutter·面试
雯0609~38 分钟前
js:循环查询数组对象中的某一项的值是否为空
开发语言·前端·javascript
bingbingyihao43 分钟前
个人博客系统
前端·javascript·vue.js
尘寰ya44 分钟前
前端面试-HTML5与CSS3
前端·面试·css3·html5
最新信息1 小时前
PHP与HTML配合搭建网站指南
前端
前端开发张小七1 小时前
每日一练:3统计数组中相等且可以被整除的数对
前端·python
天天扭码1 小时前
一杯咖啡的时间吃透一道算法题——2.两数相加(使用链表)
前端·javascript·算法
Hello.Reader1 小时前
在 Web 中调试 Rust-Generated WebAssembly
前端·rust·wasm
NetX行者1 小时前
详解正则表达式中的?:、?= 、 ?! 、?<=、?<!
开发语言·前端·javascript·正则表达式
流云一号1 小时前
Python实现贪吃蛇三
开发语言·前端·python