Slack показывает «Алиса печатает...» в реальном времени без обновления страницы. Binance отображает котировки криптовалют каждую секунду. Figma позволяет нескольким дизайнерам работать над одним файлом одновременно. Все эти функции работают через WebSocket.
HTTP работает по схеме «запрос-ответ»: клиент спрашивает, сервер отвечает. Для чата это неудобно — нужно постоянно делать новые запросы. WebSocket устанавливает постоянное двустороннее соединение: сервер сам отправляет данные клиенту когда они появляются.
const ws = new WebSocket('wss://api.example.com/ws')
// wss:// — защищённый WebSocket (как https:// для HTTP)
// ws:// — незащищённый (только для localhost в разработке)ws.onopen = () => {
console.log('Соединение установлено')
ws.send(JSON.stringify({ type: 'auth', token: userToken }))
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
handleMessage(data)
}
ws.onclose = (event) => {
console.log('Закрыто. Код:', event.code)
// 1000 — нормальное закрытие, 1006 — соединение оборвалось
}
ws.onerror = (error) => {
console.error('Ошибка WebSocket:', error)
}ws.readyState
// 0 — CONNECTING: устанавливается
// 1 — OPEN: открыто, можно отправлять
// 2 — CLOSING: закрывается
// 3 — CLOSED: закрыто// Всегда отправляй структурированные сообщения с полем type
ws.send(JSON.stringify({
type: 'chat:message',
text: 'Привет всем!',
timestamp: Date.now()
}))
// Обработка разных типов сообщений
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
switch (msg.type) {
case 'chat:message': appendMessage(msg.text, msg.author); break
case 'user:join': showNotification(msg.username + ' вошёл'); break
case 'user:typing': showTypingIndicator(msg.username); break
}
}class WebSocketClient {
constructor(url) {
this.url = url
this.ws = null
this.reconnectDelay = 1000
this.handlers = new Map()
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
this.reconnectDelay = 1000 // сброс при успехе
}
this.ws.onclose = () => {
setTimeout(() => this.connect(), this.reconnectDelay)
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
// Задержка: 1s -> 2s -> 4s -> 8s -> ... -> 30s
}
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
this.handlers.get(data.type)?.(data)
}
}
on(type, handler) { this.handlers.set(type, handler); return this }
send(type, data) { this.ws.send(JSON.stringify({ type, ...data })) }
}Ошибка 1: отправка до открытия соединения
const ws = new WebSocket('wss://example.com/ws')
ws.send('Привет!') // ОШИБКА — соединение ещё не открыто!
// Правильно
ws.onopen = () => {
ws.send('Привет!') // теперь точно открыто
}Ошибка 2: нет обработки onerror и onclose
// При обрыве ничего не произойдёт — тихий сбой
const ws = new WebSocket(url)
ws.onmessage = handleMessage
// Правильно — всегда обрабатывай все события
ws.onerror = (e) => console.error('WS Error:', e)
ws.onclose = () => reconnect()Ошибка 3: нет JSON.parse/stringify
// Неправильно
ws.send({ type: 'message', text: 'Hello' }) // '[object Object]'
// Правильно
ws.send(JSON.stringify({ type: 'message', text: 'Hello' }))
ws.onmessage = (event) => {
const data = JSON.parse(event.data) // обязательно парсим строку
}EventEmitter — паттерн Observer, лежащий в основе WebSocket-клиентов
// EventEmitter — паттерн на котором строятся WebSocket-клиенты
class EventEmitter {
constructor() {
this._events = new Map()
}
on(event, handler) {
if (!this._events.has(event)) this._events.set(event, [])
this._events.get(event).push(handler)
return this
}
off(event, handler) {
if (!this._events.has(event)) return this
this._events.set(event, this._events.get(event).filter(h => h !== handler))
return this
}
emit(event, data) {
if (!this._events.has(event)) return
this._events.get(event).forEach(h => h(data))
}
once(event, handler) {
const wrapper = (data) => {
handler(data)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
}
// Симуляция WebSocket-клиента чата
const chatClient = new EventEmitter()
chatClient
.on('message', (d) => console.log('[' + d.author + ']: ' + d.text))
.on('user:join', (d) => console.log('>> ' + d.name + ' вошёл в чат'))
.on('user:leave', (d) => console.log('<< ' + d.name + ' вышел из чата'))
chatClient.once('connect', () => console.log('Соединение установлено!'))
// Симулируем получение событий от сервера
chatClient.emit('connect', {}) // 'Соединение установлено!'
chatClient.emit('connect', {}) // ничего — once удалил обработчик
chatClient.emit('user:join', { name: 'Алиса' })
chatClient.emit('message', { author: 'Алиса', text: 'Всем привет!' })
chatClient.emit('message', { author: 'Боб', text: 'Привет, Алиса!' })
chatClient.emit('user:leave', { name: 'Алиса' })Slack показывает «Алиса печатает...» в реальном времени без обновления страницы. Binance отображает котировки криптовалют каждую секунду. Figma позволяет нескольким дизайнерам работать над одним файлом одновременно. Все эти функции работают через WebSocket.
HTTP работает по схеме «запрос-ответ»: клиент спрашивает, сервер отвечает. Для чата это неудобно — нужно постоянно делать новые запросы. WebSocket устанавливает постоянное двустороннее соединение: сервер сам отправляет данные клиенту когда они появляются.
const ws = new WebSocket('wss://api.example.com/ws')
// wss:// — защищённый WebSocket (как https:// для HTTP)
// ws:// — незащищённый (только для localhost в разработке)ws.onopen = () => {
console.log('Соединение установлено')
ws.send(JSON.stringify({ type: 'auth', token: userToken }))
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
handleMessage(data)
}
ws.onclose = (event) => {
console.log('Закрыто. Код:', event.code)
// 1000 — нормальное закрытие, 1006 — соединение оборвалось
}
ws.onerror = (error) => {
console.error('Ошибка WebSocket:', error)
}ws.readyState
// 0 — CONNECTING: устанавливается
// 1 — OPEN: открыто, можно отправлять
// 2 — CLOSING: закрывается
// 3 — CLOSED: закрыто// Всегда отправляй структурированные сообщения с полем type
ws.send(JSON.stringify({
type: 'chat:message',
text: 'Привет всем!',
timestamp: Date.now()
}))
// Обработка разных типов сообщений
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
switch (msg.type) {
case 'chat:message': appendMessage(msg.text, msg.author); break
case 'user:join': showNotification(msg.username + ' вошёл'); break
case 'user:typing': showTypingIndicator(msg.username); break
}
}class WebSocketClient {
constructor(url) {
this.url = url
this.ws = null
this.reconnectDelay = 1000
this.handlers = new Map()
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
this.reconnectDelay = 1000 // сброс при успехе
}
this.ws.onclose = () => {
setTimeout(() => this.connect(), this.reconnectDelay)
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
// Задержка: 1s -> 2s -> 4s -> 8s -> ... -> 30s
}
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
this.handlers.get(data.type)?.(data)
}
}
on(type, handler) { this.handlers.set(type, handler); return this }
send(type, data) { this.ws.send(JSON.stringify({ type, ...data })) }
}Ошибка 1: отправка до открытия соединения
const ws = new WebSocket('wss://example.com/ws')
ws.send('Привет!') // ОШИБКА — соединение ещё не открыто!
// Правильно
ws.onopen = () => {
ws.send('Привет!') // теперь точно открыто
}Ошибка 2: нет обработки onerror и onclose
// При обрыве ничего не произойдёт — тихий сбой
const ws = new WebSocket(url)
ws.onmessage = handleMessage
// Правильно — всегда обрабатывай все события
ws.onerror = (e) => console.error('WS Error:', e)
ws.onclose = () => reconnect()Ошибка 3: нет JSON.parse/stringify
// Неправильно
ws.send({ type: 'message', text: 'Hello' }) // '[object Object]'
// Правильно
ws.send(JSON.stringify({ type: 'message', text: 'Hello' }))
ws.onmessage = (event) => {
const data = JSON.parse(event.data) // обязательно парсим строку
}EventEmitter — паттерн Observer, лежащий в основе WebSocket-клиентов
// EventEmitter — паттерн на котором строятся WebSocket-клиенты
class EventEmitter {
constructor() {
this._events = new Map()
}
on(event, handler) {
if (!this._events.has(event)) this._events.set(event, [])
this._events.get(event).push(handler)
return this
}
off(event, handler) {
if (!this._events.has(event)) return this
this._events.set(event, this._events.get(event).filter(h => h !== handler))
return this
}
emit(event, data) {
if (!this._events.has(event)) return
this._events.get(event).forEach(h => h(data))
}
once(event, handler) {
const wrapper = (data) => {
handler(data)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
}
// Симуляция WebSocket-клиента чата
const chatClient = new EventEmitter()
chatClient
.on('message', (d) => console.log('[' + d.author + ']: ' + d.text))
.on('user:join', (d) => console.log('>> ' + d.name + ' вошёл в чат'))
.on('user:leave', (d) => console.log('<< ' + d.name + ' вышел из чата'))
chatClient.once('connect', () => console.log('Соединение установлено!'))
// Симулируем получение событий от сервера
chatClient.emit('connect', {}) // 'Соединение установлено!'
chatClient.emit('connect', {}) // ничего — once удалил обработчик
chatClient.emit('user:join', { name: 'Алиса' })
chatClient.emit('message', { author: 'Алиса', text: 'Всем привет!' })
chatClient.emit('message', { author: 'Боб', text: 'Привет, Алиса!' })
chatClient.emit('user:leave', { name: 'Алиса' })Реализуй класс EventEmitter (паттерн Observer — основа WebSocket-клиентов): on(event, handler), off(event, handler), emit(event, data). Дополнительно реализуй метод once(event, handler) — обработчик вызывается только один раз, затем автоматически удаляется.
on: if (!this._events.has(event)) this._events.set(event, []); this._events.get(event).push(handler); return this. off: используй filter. once: const wrapper = (data) => { handler(data); this.off(event, wrapper) }; return this.on(event, wrapper).