前端实时更新数据的几种方式

我很苦涩的 2024-09-06 08:03:04 阅读 87

本文章copy进击的大葱,仅做记录使用。

后端代码请转链接查看原作者文章。

一、轮询

简单来说轮询就是客户端不停地调用服务端接口以获得最新的数据。

前端代码

<code><!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta http-equiv="X-UA-Compatible" content="IE=edge">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>前端实时更新数据实现</title>

</head>

<body>

<div id="wrap"></div>code>

</body>

<script>

//前端存储事件列表

const events = [];

//最新的数据时间戳

let timestamp = 0;

//获取数据函数

const fetchLatestEvents = async (timestamp) => {

// 获取最新的事件

const body = await window.fetch(`http://localhost:8080/events?timestamp=${timestamp}`);

if (body.ok) {

const json = await body.json()

return json

} else {

console.error('failed to fetch')

}

}

//设置轮询,3秒发一次请求

function App() {

const timer = setInterval(async () => {

const latestEvents = await fetchLatestEvents(timestamp);

if (latestEvents && latestEvents.length) {

timestamp = latestEvents[latestEvents.length - 1].timestamp;

events.push(...latestEvents);

renderEl(events);

}

}, 3000)

}

//把数据渲染到页面上

function renderEl(arr) {

let str = '';

arr.map(v => {

str += `

<div>id:${v.id};-------timestamp:${v.timestamp} <div>

`

})

const el = document.getElementById('wrap');

el.innerHTML = str;

}

//执行函数

App();

</script>

</html>

        前端每隔3s向后端请求一次,请求得相当频繁,并且在后端没有产生新数据的时候,很多请求的返回值是空的,也就是说大多数的网络资源都被浪费了。

轮询的优缺点

        从上面的代码我们可以看出,短轮询这个技术方案最大的优点就是实现简单,而它的缺点也很明显:

1. 无用的请求多: 因为客户端不知道服务端什么时候有数据更新,所以它只能不停地询问服务端,如果服务端的数据更新并不频繁的话,这些请求大多都是无用的。无用的请求会导致服务端的带宽占用增加,消耗服务端资源,同时如果客户端是一些移动设备的话,耗电速度也会很快。

2.数据实时性差: 由于不想消耗太多客户端或者服务端的资源,我们通常在实现轮询时不会拿到上一个请求的结果后立即发送第二个请求,这就导致了即使服务端的数据更新了,我们客户端还是需要一段时间才能拿到最新的数据,这对于一些数据实时性要求高的应用例如IM系统是致命的。

使用场景

        一般生产级别的应用都不会使用短轮询这个方案,除非你只是写一些给少数人用的系统。

二、长轮询

        看完了上面关于短轮询的介绍,我们知道了轮询有两个重大的缺陷:一个是无用请求过多,另外一个是数据实时性差。为了解决这两个问题,某些聪明的程序员就发明了另外一个方案:长轮询,客户端发起请求后,服务端发现当前没有新的数据,这个时候服务端没有立即返回请求,而是将请求挂起,在等待一段时间后(一般为30s或者是60s),发现还是没有数据更新的话,就返回一个空结果给客户端。客户端在收到服务端的回复后,立即再次向服务端发送新的请求。这次服务端在接收到客户端的请求后,同样等待了一段时间,这次好运的是服务端的数据发生了更新,服务端给客户端返回了最新的数据。客户端在拿到结果后再次发送下一个请求,如此反复。

前端代码

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta http-equiv="X-UA-Compatible" content="IE=edge">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>前端实时更新数据实现</title>

</head>

<body>

<div id="wrap"></div>code>

</body>

<script>

//前端存储事件列表

const events = [];

//最新的数据时间戳

let timestamp = 0;

//获取数据函数

const fetchLatestEvents = async (timestamp) => {

// 获取最新的事件

const body = await window.fetch(`http://localhost:8080/events?timestamp=${timestamp}`);

if (body.ok) {

const json = await body.json()

return json

} else {

console.error('failed to fetch')

}

}

const App = async () => {

const latestEvents = await fetchLatestEvents(timestamp);

if (latestEvents && latestEvents.length) {

timestamp = latestEvents[latestEvents.length - 1].timestamp;

events.push(...latestEvents);

renderEl(events);

}

}

//把数据渲染到页面上

function renderEl(arr) {

let str = '';

arr.map(v => {

str += `

<div>id:${v.id};-------timestamp:${v.timestamp} <div>

`

})

const el = document.getElementById('wrap');

el.innerHTML = str;

}

function getData() {

//执行函数

App().then(() => {

getData();

}).catch(err => {

console.log(err, 'err');

})

}

getData();

</script>

</html>

值得注意的是,这个时候,我们打开浏览器的调试工具可以发现浏览器每一次发出的请求都不会立马收到回复,而是pending一段时间后(大概是5秒)才会有结果,并且结果里面都是有数据的。

长轮询的优缺点

        长轮询很完美地解决了短轮询的问题,首先服务端在没有数据更新的情况下没有给客户端返回数据,所以避免了客户端大量的重复请求。再者客户端在收到服务端的返回后,马上发送下一个请求,这就保证了更好的数据实时性。不过长轮询也不是完美的:

服务端资源大量消耗: 服务端会一直hold住客户端的请求,这部分请求会占用服务器的资源。对于某些语言来说,每一个HTTP连接都是一个独立的线程,过多的HTTP连接会消耗掉服务端的内存资源。

难以处理数据更新频繁的情况: 如果数据更新频繁,会有大量的连接创建和重建过程,这部分消耗是很大的。虽然HTTP的keep-alive字段可以解决一部分问题,不过每次拿到数据后客户端都需要重新subscribe,因此相对于WebSocket和SSE它多了一个发送新请求的阶段,对实时性和性能还是有影响的

应用场景

        从网上找的资料来看之前的WebQQ和Web微信都是基于长轮询实现的,现在是不是我就不知道了,有兴趣的读者可以自行验证一下。

三、websocket

        上面说到长轮询不适用于服务端资源频繁更新的场景,而解决这类问题的一个方案就是WebSocket。用最简单的话来介绍WebSocket就是:客户端和服务器之间建立一个持久的长连接,这个连接是双工的,客户端和服务端都可以实时地给对方发送消息。首先客户端会给服务端发送一个HTTP请求,这个请求的Header会告诉服务端它想基于WebSocket协议通信,如果服务端支持升级协议的话,会给客户端发送一个Switching Protocal的响应,它们之间后面都是基于WebSocket协议来通信了。

前端代码

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta http-equiv="X-UA-Compatible" content="IE=edge">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>前端实时更新数据实现</title>

</head>

<body>

<div id="wrap"></div>code>

</body>

<script>

//前端存储事件列表

const events = [];

//最新的数据时间戳

let timestamp = 0;

const ws = new WebSocket(`ws://localhost:8080/ws?timestamp=${timestamp}`);

//监听链接事件

ws.addEventListener('open', () => {

console.log('connect');

})

//报错信息

ws.addEventListener('error', () => {

console.log('error');

})

//关闭事件

ws.addEventListener('close', () => {

console.log('close');

})

//信息发送事件

ws.addEventListener('message', (ev) => {

const latestEvents = JSON.parse(ev.data)

if (latestEvents && latestEvents.length) {

timestamp = latestEvents[latestEvents.length - 1].timestamp;

events.push(...latestEvents);

renderEl(events);

}

})

//把数据渲染到页面上

function renderEl(arr) {

let str = '';

arr.map(v => {

str += `

<div>id:${v.id};-------timestamp:${v.timestamp} <div>

`

})

const el = document.getElementById('wrap');

el.innerHTML = str;

}

</script>

</html>

        打开Chrome的网络调试工具点击ws,你会发现客户端和服务端只有一个websocket连接,它们所有的通信都是发生在这个连接上面的:

WebSocket的优缺点

总的来说,我认为WebSocket有下面这些优点:

客户端和服务端建立连接的次数少:理想情况下客户端只需要发送一个HTTP升级协议就可以升级到WebSocket连接,后面所有的消息都是通过这个通道进行通信,无需再次建立连接。

消息实时性高:由于客户端和服务端的连接是一直建立的,所以当数据更新的时候可以马上推送给客户端。

双工通信:服务端和客户端都可以随时给对方发送消息,这对于本文的其它三种方案都是很难做到的。

适用于服务端数据频繁更新的场景:和长轮询不同,服务端可以随时给客户端推送新的信息,而客户端在拿到信息后不需要重新建立连接或者发送请求,因此WebSocket适合于数据频繁更新的场景。

同样WebSocket也不是完美的,它有下面这些问题:

1. 扩容麻烦:基于WebSocket的服务是有状态的。这就意味着在扩容的时候很麻烦,系统设计也会较复杂。

2. 代理限制:某些代理层软件(如Nginx)默认配置的长连接时间是有限制的,可能只有几十秒,这个时候客户端需要自动重连。要想突破这个限制你就需要将从客户端到服务端之间所有的代理层的配置都改掉,在现实中这可能是不可行的。

应用场景

WebSocket的应用场景是一些实时性要求很高的而且需要双工通信的系统例如IM软件等。

四、Server-Sent Events

Server-Sent Events简称SSE,是一个基于HTTP协议的服务端向客户端推送数据的技术。

客户端向服务端发起一个持久化的HTTP连接,服务端接收到请求后,会挂起客户端的请求,有新消息时,再通过这个连接将数据推送给客户端。这里需要指出的是和WebSocket长连接不同,SSE的连接是单向的,也就是说它不允许客户端向服务端发送消息。

前端代码

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta http-equiv="X-UA-Compatible" content="IE=edge">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>前端实时更新数据实现</title>

</head>

<body>

<div id="wrap"></div>code>

</body>

<script>

//前端存储事件列表

const events = [];

//最新的数据时间戳

let timestamp = 0;

const source = new EventSource(`http://localhost:8080/subscribe?timestamp=${timestamp}`)

//监听链接事件

source.onopen = () => {

console.log('connected')

}

//监听信息推送事件

source.onmessage = ev => {

const latestEvents = JSON.parse(ev.data)

if (latestEvents && latestEvents.length) {

timestamp = latestEvents[latestEvents.length - 1].timestamp;

events.push(...latestEvents);

renderEl(events);

}

}

//监听错误事件

source.addEventListener('error', (e) => {

console.error('Error: ', e);

})

//把数据渲染到页面上

function renderEl(arr) {

let str = '';

arr.map(v => {

str += `

<div>id:${v.id};-------timestamp:${v.timestamp} <div>

`

})

const el = document.getElementById('wrap');

el.innerHTML = str;

}

</script>

</html>

        打开Chrome的网络调试工具,会发现HTTP请求变成了EventStream类型,而且服务端给客户端所有的事件推送都在这个连接上,而无需建立新的连接。

SSE的优缺点

在我看来,SSE的技术有下面的优点:

连接数少: 客户端和服务端只有一个持久的HTTP连接,因此性能也是很好的。

数据实时性高: 它比长轮询更加实时,因为服务端和客户端的连接是持久的,所以有新消息的话可以直接推送到客户端。

SSE的问题也很明显:

单向通信: SSE长连接是单向的,不允许客户端给服务端推送数据。代理层限制: 和WebSocket一样会遇到代理层配置的问题,配置错误的话,客户端需要不断和服务端进行重连。

使用场景

        SSE技术适合一些只需要服务端单向推送事件给客户端的场景,例如股票行情推送软件。

总结

        在本篇文章中我通过图解和实际代码给大家介绍了四种不同的和服务端保持数据同步的方案,看完本篇文章后,相信你后面再遇到类似的需求时,除了短轮询你会有更多的方案可以选择。同时这里也还是要强调一下:任何一种技术都不是瑞士军刀,都有自己适用和不适用的场景,一定要根据自己的实际情况进行取舍,从而选择最适合的方案,千万不要为了用某个技术而用某个技术!

注:转载自:https://juejin.cn/post/7139684620777291807



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。