1. 为什么前端要用 WebSocket:先从 HTTP 轮询的痛点说起
做前端这几年,真正逼着我去研究 JavaScript 连接 WebSocket 的,不是网上那些花哨的 Demo,而是业务方的两句话:“页面上的状态要实时变”“消息不能等刷新才出现”。在线客服、协作白板、股票行情、扫码登录、打印机任务状态……这些场景一旦背上“实时”两个字,用普通 HTTP 请求去做数据更新,痛点会非常明显:你不知道订单什么时候被接单,不知道对方什么时候发来消息,只能让页面每隔几秒问一次服务器“有变化了吗”,也就是轮询。
轮询不是不能做,但并发一旦上来,你会看到一堆 502、请求超时、服务器 CPU 飙升,前端还要处理定时器错乱、清理不及时引发的内存泄漏。更离谱的是,哪怕服务器那边没有任何新消息,每一轮空请求照样会把 HTTP 头、Cookie、鉴权逻辑全部重新走一遍。后来我又尝试过长轮询,虽然比普通轮询强一些,让服务器先“挂住”请求,等有数据再返回,但每次连接到期后还是得重新发请求,本质依然是一条单向管道,服务端没法主动往客户端推数据。
WebSocket 的出现,正是把“服务端主动推送”这件事做成标准。它和 HTTP 一样基于 TCP,但只需要一次握手,就能在客户端和服务端之间维持一条全双工通道。所谓全双工,就是两端都能随时发数据,不用再分轮流。JavaScript 这门语言在浏览器里天生内置了 WebSocket 构造函数,Node.js 环境里也有官方实现和 ws 这样的第三方库。你不需要装任何插件,只需要 new WebSocket(url),就能把一个实时通道握在手里。
这篇文章适合的读者,不只是已经熟练使用框架的前端工程师。哪怕你刚学 JavaScript 没多久,只要能看得懂函数和对象,跟着文章里的代码一步步敲,也能把连接跑起来。我会把连接时涉及的 URL 格式、状态码、握手过程、事件回调、重连策略、消息格式设计、部署时常见的坑全部讲清楚,而且会尽量解释每一步“为什么这么写”,而不是丢给你一段能跑但不理解的黑盒代码。
1.1 WebSocket 相比 HTTP 轮询,到底赢在哪里
先说一个容易被忽略的点:WebSocket 的首轮握手,其实就是一个带特殊请求头的 HTTP Upgrade 请求。浏览器发一个 GET 请求,服务端看到里面有 Upgrade: websocket,如果同意切换协议,就会返回 101 Switching Protocols,之后这条 TCP 连接就从 HTTP 协议切换成了 WebSocket 协议。
所以它不是完全脱离 HTTP 的另一种东西,而是借助 HTTP 完成渠道协商。这样做有一个很明显的好处:连接建立之前,你依旧可以通过 HTTP 的 Header 传递 Cookie、Token 等鉴权信息,网关、负载均衡器也能先走同一套规则。
但连接建立之后,WebSocket 的传输效率和 HTTP 就完全不是一个量级了。用 HTTP 轮询,每次请求至少有几十个字节甚至几百字节的 Header 开销,这些 Header 大多数是重复的;而 WebSocket 数据帧的最小开销只有 2 个字节左右,如果消息内容很短,省下来的网络消耗非常可观。更重要的是,HTTP 协议是“一问一答”,客户端不发请求,服务器就不能回消息;WebSocket 打破了这层限制。
拿扫码登录举例。以前我实现扫码登录,可能需要前端每 3 秒轮询一次“二维码状态”,直到用户扫码并确认。换成 WebSocket 后,用户手机确认的瞬间,服务器直接推送一条 { action: 'loginSuccess', token: 'xxx' },网页马上跳转,中间没有任何等待。对于消息推送场景,这个模型直观得多:客户端不用反复问,服务端有事件就推过来。
1.2 WebSocket 连接的生命周期和核心机制
JavaScript 里的 WebSocket 对象,其实内部维护了一个状态机。你通过 readyState 属性可以看到当前连接处于哪个阶段:
| readyState | 常量名 | 含义 |
|---|---|---|
| 0 | CONNECTING | 正在握手,还没建立连接 |
| 1 | OPEN | 连接已建立,可以收发数据 |
| 2 | CLOSING | 正在关闭,客户端或服务端发起了关闭流程 |
| 3 | CLOSED | 连接已经关闭或根本没能建立 |
我第一次看这个状态机的时候,觉得前端写连接无非是四件事:连接成功、收到消息、连接出错、连接关闭。真正用起来才发现,很多线上问题恰恰出在对状态的不重视。比如根本没有判断 readyState 是不是 1 就直接调用 send(),浏览器会直接抛异常,而你会花很长时间去查为什么代码逻辑没进来。
关于消息格式,WebSocket 协议支持文本帧和二进制帧。文本帧的内容是 UTF-8 字符串,在 JavaScript 里表现为普通的 string 类型;二进制帧则可以是 Blob 或 ArrayBuffer,具体拿到哪一种,由 binaryType 属性决定。做浏览器端开发时,如果你要传输 JSON 格式的数据,最简单的方式就是 JSON.stringify() 后以文本帧发送,收到消息后再 JSON.parse() 解析。这样写业务比较直观,做多人协作、聊天这类应用时错误也容易排查。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JavaScript 连接 WebSocket 的入门写法与状态处理
很多教程一上来就让你填完整代码,但我觉得先拆开看一个连接对象的生命周期,后面理解封装模块会轻松很多。JavaScript 里发起连接只需要一行:
javascript复制const ws = new WebSocket('ws://localhost:8080/chat');
这一行代码执行的同时,浏览器会立刻去发起握手。注意:WebSocket 的 URL 是以 ws:// 或 wss:// 开头的,不能用 http://。如果你要加密传输,就必须使用 wss://,它的作用类似于 HTTPS,可以防止中间人窃听或篡改内容。
2.1 URL、子协议和二进制类型这些参数怎么定
new WebSocket(url) 还有第二个可选参数 protocols。这个参数不是给自己看的“标签”,而是一个字符串或字符串数组,用来告诉服务器“客户端期望使用哪个子协议”。比如你后端同时支持 JSON 协议和 MessageP
