当前解决服务端推送的方案有这几个:
- 客户端长轮询(不推荐使用)
- websocket双向连接
- iframe永久帧(不推荐使用)
- EventSource
长轮训虽然可以避免短轮训造成的服务端过载,但在服务端返回数据后仍需要客户端主动发起下一个长轮训请求,等待服务端响应,这样仍需要底层的连接建立而且服务端处理逻辑需要相应处理,不符合逻辑上的流程简单的服务端推送;
websocket连接相对而言功能最强大,但是它对服务器的版本有要求,在可以使用websocket协议的服务器上尽量采用此种方式;
iframe永久帧则是在在页面嵌入一个专用来接受数据的iframe页面,该页面由服务器输出相关信息,如,服务器不停的向iframe中写入类似的script标签和数据,实现另一种形式的服务端推送。不过永久帧的技术会导致主页面的加载条始终处于“loading”状态,体验很差。
HTML5规范中提供了服务端事件EventSource,浏览器在实现了该规范的前提下创建一个EventSource连接后,便可收到服务端的发送的消息,这些消息需要遵循一定的格式,对于前端开发人员而言,只需在浏览器中侦听对应的事件皆可。
相比较上文中提到的3中实现方式,EventSource流的实现方式对客户端开发人员而言非常简单,兼容性上出了IE系的浏览器(IE、Edge)外其他都良好;对于服务端,它可以兼容老的浏览器,无需upgrade为其他协议,在简单的服务端推送的场景下可以满足需求。在浏览器与服务端需要强交互的场景下,websocket仍是不二的选择。
客户端从服务器接受事件
服务器发送事件 API 也就是 EventSource 接口,在你创建一个新的 EventSource 对象的同时,你可以指定一个接受事件的 URI。例如:
const evtSource =newEventSource("ssedemo.php");
备注: 从 Firefox 11 开始,EventSource开始支持CORS.虽然该特性目前并不是标准,但很快会成为标准。
如果发送事件的脚本不同源,应该创建一个新的包含 URL 和 options 参数的EventSource对象。例如,假设客户端脚本在 example.com 上:
const evtSource =newEventSource("//api.example.com/ssedemo.php",{withCredentials:true});
一旦你成功初始化了一个事件源,就可以对 message 事件添加一个处理函数开始监听从服务器发出的消息了:
evtSource.onmessage=function(event){const newElement = document.createElement("li");const eventList = document.getElementById("list");
newElement.innerHTML ="message: "+ event.data;
eventList.appendChild(newElement);}
上面的代码监听了那些从服务器发送来的所有没有指定事件类型的消息 (没有event字段的消息),然后把消息内容显示在页面文档中。
你也可以使用addEventListener()方法来监听其他类型的事件:
evtSource.addEventListener("ping",function(event){const newElement = document.createElement("li");const time =JSON.parse(event.data).time;
newElement.innerHTML ="ping at "+ time;
eventList.appendChild(newElement);});
这段代码也类似,只是只有在服务器发送的消息中包含一个值为"ping"的event字段的时候才会触发对应的处理函数,也就是将data字段的字段值解析为 JSON 数据,然后在页面上显示出所需要的内容。
页面代码案例:
<!DOCTYPEhtml><head><metacharset="UTF-8"><title>测试</title></head><body><div>服务器端推送测试</div><divid="serverSendEventDiv"></div></body><scripttype="text/javascript">if(window.EventSource){const source =newEventSource("push");let s ='';// 监听打开连接
source.addEventListener('open',function(e){
console.log("连接打开")},false);
source.addEventListener('message',function(e){
s += e.data +'<br/>';
document.getElementById("serverSendEventDiv").innerHTML = s;})// 监听关闭连接
source.addEventListener('close',function(e){if(e.readyState === EventSource.CLOSED){
console.log("连接关闭")}else{
console.log(e.readyState)}},false);
source.addEventListener("error",function(err){
console.log(JSON.stringify(err))
console.log(err)// 类似的返回信息验证,这里是实例
err && err.status ===401&& console.log('not authorized')})}else{alert("你的浏览器不支持sse")}</script>
服务器端如何发送事件流
服务器端发送的响应内容应该使用值为
text/event-stream
的 MIME 类型。每个通知以文本块形式发送,并以一对换行符结尾。有关事件流的格式的详细信息,请参见事件流格式。
官方文档给了一个php版本代码示例:
date_default_timezone_set("America/New_York");header("Cache-Control: no-cache");header("Content-Type: text/event-stream");$counter=rand(1,10);while(true){// Every second, send a "ping" event.echo"event: ping\n";$curDate=date(DATE_ISO8601);echo'data: {"time": "'.$curDate.'"}';echo"\n\n";// Send a simple message at random intervals.$counter--;if(!$counter){echo'data: This is a message at time '.$curDate."\n\n";$counter=rand(1,10);}ob_end_flush();flush();sleep(1);}
上面的代码会让服务器每隔一秒生成一个事件流并返回,其中每条消息的事件类型为"ping",数据字段都使用了 JSON 格式,数组字段中包含了每个事件流生成时的 ISO 8601 时间戳。而且会随机返回一些无事件类型的消息。
java代码示例
packagecom.hj.ServerSendEvent;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.ResponseBody;importjava.util.Random;/**
* describe
*
* @author huangjuan
* @date 2023/2/15 9:59
*/@ControllerpublicclassServerSentEventController{@RequestMapping(value ="/push",produces ="text/event-stream")@ResponseBodypublicStringpushToBrowser(){Random random =newRandom();try{Thread.sleep(5000);}catch(InterruptedException e){thrownewRuntimeException(e);}return"data: server send event push message test:"+ random.nextInt()+"\n\n";}}
post的方式进行eventSource请求
new EventSource
默认只支持get请求
用post的方式请求eventSource,常用的就是通过fetchEventSource这个库来实现
npm 仓库地址:https://www.npmjs.com/package/@microsoft/fetch-event-source
import{ fetchEventSource }from'@microsoft/fetch-event-source';const ctrl =newAbortController();fetchEventSource('/api/sse',{method:'POST',headers:{'Content-Type':'application/json',},body:JSON.stringify({foo:'bar'}),signal: ctrl.signal,});
但是我的项目是简单的html项目,需要把npm包转换成js文件
npm包生成单个js文件并压缩
这里使用一个工具
browserify
,
npm install -g browserify
使用
browserify main.js -o bundle.js
压缩出来的js文件,有时需要把抛出来的方法挂在window下,然后在html里引入使用,看情况而定。
如果要对js文件 压缩混淆
npm i browserify minify crypto-js
browserify -r crypto-js -s CryptoJS > crypto.js // -r 指定npm包名 -s 在代码中使用的模块名
minify crypto.js > crypto.mini.js // 压缩js文件
请求失败后一直重新连接如何暂停
这个我在监听错误的回调里面 抛出了一个错误
throw new Error(err)
,发现可以解决
fetchEventSource(`${this.baseUrl}/testurl`,{method:'GET',signal: that.abortController.signal,headers:{'Content-Type':'application/json','devicetype':'pc','token': that.token
},asynconopen(response){if(response.ok){
console.log('连接了');}else{}},onmessage(msg){if(msg.event ==='FatalError'){thrownewError(msg.data);}if(msg.data !=='[DONE]'){}},onclose(){},onerror(err){thrownewError(err)}});
https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource
https://blog.csdn.net/suwk1009/article/details/129323226
https://www.npmjs.com/package/@microsoft/fetch-event-source
版权归原作者 拾荒李 所有, 如有侵权,请联系我们删除。