前端通信:SSE设计方案(二)--- 服务器推送技术的实践以及一些应用场景的demo(包括在线及时聊天系统以及线上缓存更新,代码热修复案例)

967 阅读8分钟
原文链接: zhuanlan.zhihu.com
同步更新博客:前端通信:SSE设计方案(二)--- 服务器推送技术的实践以及一些应用场景的demo(包括在线及时聊天系统以及线上缓存更新,代码热修复案例)

距离上一篇博客,这篇文章的发布大概过了整整三个月。我也从饿了么度过了试用期,成为了正式员工。刚进来恰好遇到项目底层改造和迁移,将项目从angular全部迁移到vue上,所以适应这边的节奏和业务的开发任务。而且这段事件用过mint-ui这个h5的框架,感觉太老了,想自己开发一套ui组件了,所以一直忙呀忙。顺带最近绝地求生比较火,然后也拉了几个小伙伴一起玩了好长时间,所以节奏有点慢了。下面废话不多说了,直接进入主题。

上一篇博客介绍了基础的纯概念,这篇文章纯粹偏技术实践,需要理解一些玩意的。技术介绍

  • 客户端基础类库代码 -- SSE.js和ajax.1.7.js    客户端创建连接和定义监听的代码 以及结合ajax完成双工通道,完成双向都可推送
  • node -- SSE_server.js      node服务器代码,处理请求和消息推送
  • nginx     作为测试服务器进行浏览器和局域网测试

官方规则和标准:HTML Standardhtml.spec.whatwg.org

EventSource developer.mozilla.org 图标 

使用服务器发送事件developer.mozilla.org图标


SSE.js 代码改动(比较上一个版本)

// 抛出对象
  var output = {
    create:function (options) {
      var param = tool.initParam(options),sendData = '';

      if (param.data){
        tool.each(param.data, function (item, index) {
          sendData += (index + "=" + item + "&")
        });
        sendData = sendData.slice(0, -1);
      }

      var es = new EventSource(param.url+'?'+sendData);

      es.addEventListener('open',function (e) {
        param.openEvent(e)
      });

      es.addEventListener('message',function (e) {
        param.messageEvent(e)
      });

      es.addEventListener('error',function (e) {
        param.errorEvent(e)
        es.close()  // 关闭连接
      });

      // 创建用户自定义事件
      if (param.customEvent.length > 0){
        tool.each(param.customEvent,function (item) {
          es.addEventListener(item.name,item.callback);
        })
      }
    }
  }

改动:

针对上一个版本,增加了数据验证以及一些参数的传递。虽然开启withCredentials可以传递信息的凭据,比如cookie,但是毕竟一个url,也是可以用url带参传递过去的,所以额外增加了这个玩意。so,这样我们就可以在非ie系列的浏览器上痛快的翱翔了。

PS:上面代码为创建的核心代码,其他代码可以到github上看,或者直接npm安装好了看。


客户端的准备工作搞好了,下一步就是服务器的工作了,因为一个技术肯定是多个技术配合才能完成的,不懂服务器的前端,不是好前端,这是我一项认可的事情。


下面为简易的node测试代码,没有那么追求精简,为了测试搞的。

var http = require("http");
var url=require('url');
var qs=require('querystring');//解析参数的库
var sendObject = {}, count = 0;
var gerry = {}

// 向除自己的所有人推送信息
function sendAll(data) {
  for (let index in sendObject){
    if (data.name !== index){
      sendObject[index].write("retry: " + data.retry + "\n");
      if (data.event){
        sendObject[index].write("event: " + data.event + "\n");
      }

      let sengData = {
        author:data.name,
        data:data.data
      }
      sendObject[index].write("data: " + JSON.stringify(sengData) + "\n\n");
    }
  }
}

// 聊天系统的demo推送测试
http.createServer(function (req, res) {

    var arg1=url.parse(req.url,true).query;
    console.log(arg1.name);
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    res.setHeader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    res.writeHead(200, {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive"
    });

    res.setTimeout(5000,()=>{
        res.write(":this is test")
    })



    sendObject[arg1.name] = res;
}).listen(8074);

// ajax双工执行发送机制
http.createServer(function (req, res) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    res.setHeader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    res.writeHead(200,{
        "content-type":"application/json; charset=utf-8"
    });
  var arr = []
    new Promise(function(x,xx){
      req.on("data",function(data){
        arr.push(data);
      });

      req.on("end",function(){
        var data= Buffer.concat(arr).toString(),ret;
        try{
          ret = JSON.parse(data);
          x(ret)
        }catch(err){}
      })
  }).then(function (req) {
      var data = {
        retry: 10000,
        name: req.name,
        data: req.message,
        event:req.event
      }
      sendAll(data)
      // res.write(Buffer.concat(arr).toString())

    })
  res.end()
}).listen(8075)


var cacheUpdate = {}

// 缓存更新和线上正在使用的代码热修复和强制用户重新请求去拉取最新代码
http.createServer(function (req, res) {

  var arg1=url.parse(req.url,true).query;
  console.log(arg1.name);
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  res.setHeader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive"
  });

  res.setTimeout(5000,()=>{
    res.write(":this is test")
  })
  cacheUpdate[arg1.name] = res;
}).listen(8076);

// 触发推送的动作
http.createServer(function (req, res) {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  res.setHeader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  res.writeHead(200,{
    "content-type":"application/json; charset=utf-8"
  });
  var arr = []
  new Promise(function(x,xx){
    req.on("data",function(data){
      arr.push(data);
    });

    req.on("end",function(){
      var data= Buffer.concat(arr).toString(),ret;
      try{
        ret = JSON.parse(data);
        x(ret)
      }catch(err){}
    })
  }).then(function (req) {
    var data = {
      retry: 10000,
      name: req.name,
      data: req.message,
      event:req.event
    }

    cacheUpdate[data.name].write("retry: " + data.retry + "\n");
    cacheUpdate[data.name].write("event: " + data.event + "\n");
    cacheUpdate[data.name].write("data: " + data.data + "\n\n");

  })
  res.end()
}).listen(8077)

最重要的环节开始了,下面就是我的show time。

1.  聊天系统的demo

  介绍:既然服务器有了主动推送给客户端的能力,那么最重要的基础场景就是交流。所以这个聊天系统demo应运而生

  使用的类库:ajax类库+sse类库(这里的ajax随便大家用什么通信类库,我这里使用的是我自己第一阶段研究的类库)

  demo查看:github.com/GerryIsWarr…

测试(模拟了2个人,只要服务器吃的消,多少人都可以):

测试2个用户的聊天场景
第二个用户接受到的推送
监控chrome的请求

总结: 这个聊天系统的demo,创建了2个用户,用左边用户进行发送,监控右边用户的控制台的network,可以查看到通信的请求,在左边用户每次发完消息之后,右边用户每次都接受到数据,然后处理到聊天系统界面。其实sse后端底层也类似于长连接,只是多了一个服务器可以反向推的动作。


2. 线上客户端缓存的更新demo

  介绍:因为前端浏览器的机制,将所有用的数据请求过来,没有请求的页面也能跑起来。所以对于做前端优化的同学就明白一件事,希望用户能更多的缓存命中我们存储在用户浏览器中的一些数据,这样对于第一次加载之后,如果一些缓存数据没动的data,直接取出来,渲染到用户浏览器中。这样给用户的感觉就是秒开的感觉(大家可以参考京东网站的首页优化,首屏啊,二次打开等等)

  使用类库:SSE.js+原生js+postMan去触发推送

  demo查看:github.com/GerryIsWarr…

测试(模拟localStory缓存用户数据更新)

本地的基础缓存(以前存储过的)
服务器推送过来的新缓存
本地localStory的更新
postman推送的信息

总结:测试页面,首先我在代码中固定写了一个dom的我是基础缓存的数据,存到localStory中。然后对于这个打开我们的网页在线上的用户,我使用了postman往我的借口里发送数据,触发推送的动作,比如我的demo中就是发送了一个‘我试更新过的混存数据’,然后推送到客户端,然后客户端接收到这个event,然后更新缓存。这样就完成了线上缓存更新的方式。这样避免了让用户再刷一次页面(重新请求页面数据,比对缓存版本啥的),毕竟用户,才是我们的上帝。


3. 线上代码热修复以及生产版本更新提示用户重新拉取页面

  介绍:对于现在更多的spa来说,浏览器下载结束了代码,你使用的代码就是浏览器下载的,如果这块前端代码是有问题的,那么生产跑得代码都是有问题代码,除非用户手动刷新页面,重新请求一个正确的生产代码。

  demo查看:github.com/GerryIsWarr…

测试(模拟一个线上案例):

线上计算金额,打了8折
推送新的折扣,9折
用户重新计算为9折的结果

总结:这个案例就是这样,之前我们写死了线上代码要打8折,所以我们最后计算金额的时候就是按照8折去计算的。但是产品过来说,不应该这样啊,这TM不是打9折吗?这样公司会亏损严重的。然后大家都蒙蔽,修正发布。然后发现用户的页面还是8折,用户没刷新。尴尬.....

so,通过正在线上的推送,来进行代码的热修复。比如我推送了一个9折的信息,然后客户端接受处理了,哪怕正在线上的用户都按正确的9折来计算。挽回了公司的损失。

PS:如果线上改动太多太多了,不好进行局部热修复方案,只能让正在用的用户强制更新,拉新数据。

让用户强制更新,止损

就像上图一样,用户点确定就重新刷新页面,拉取新代码。


浏览器的测试:兼容结果:chrome,safair,opera,firefox都可以跑


结语:这是sse技术研究的第二阶段,可以在非IE系列的浏览器上都跑起来了,而且针对不同场景的操作和优化都有很多的想法。每个想法真正写成一个成熟的东西,都能让前端变得更好。第三阶段,暂时定为SSE的收尾阶段,解决一些兼容性问题和以后用户提出的新问题。其实,搞前端这么久了,我感觉针对每个技术本身,都不是什么很牛逼和好玩的技术,但是把许多单个的技术融合起来,这样就会发现新的大陆一样,能创造奇迹。

github地址: github.com/GerryIsWarr… 如果感觉好,或者能解决你的问题,可以点歌star肯定我

最近感悟最深的一句话:我可能推动不了前端技术的进步,但是我可以让前端技术变得更美好.....共勉