读一下@microsoft/fetch-event-source源码

做过两个大模型聊天的项目,也是从那时起知道了SSE(Server-Sent Events),进而又知道了@microsoft/fetch-event-source。

今天有空把自己知道的做一个总结,便于以后查阅,主要是浅读一下@microsoft/fetch-event-source这个库的源码。

基本的SSE概念可以参考https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html和https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events。

既然有了浏览器原生支持的EventSource为什么还要@microsoft/fetch-event-source,可以参考@microsoft/fetch-event-source中提到的
在这里插入图片描述
大体意思是:原生EventSource只有get请求参数只能通过url传递,不能传递自定义的请求头,重试机制我们不能插手。而它解决了以上的问题。同时你还能拿到Response对象。

现在有很多前端项目,跟后端的鉴权机制是通过在请求头中添加Authorization,用原生的EventSource就不行。

接下来让我们看一下@microsoft/fetch-event-source源码吧

0. 克隆代码

git clone https://github.com/Azure/fetch-event-source.git

1. 目录结构

在这里插入图片描述
src目录中有以上4个文件,parse.spec.ts是用来做单元测试的,这里就不看了。index.ts只是简单的导出,也不看了。fetch.ts负责发送请求,parse.ts负责把请求返回的数据解析出消息。

2. fetch.ts

这个文件中最主要的就是fetchEventSource函数,也是这个库的最主要的内容。它的主要流程是调用fetch发送请求,用parse.ts中的函数解析响应返回的流得到一个个消息(事件)

FetchEventSourceInit是fetchEventSource函数的第二个参数的类型定义,它包括

字段含义
headers请求头,Record<string, string>
onopen响应回来调用的回调
onmessage接收到消息时的回调
onclose响应结束时的回调
onerror报错时的回调
openWhenHidden页面隐藏时是否依旧开这连接
fetch自定义的fetch,默认使用window.fetch

方法defaultOnOpen, 默认的onopen回调,判断响应的Content-Type是否text/event-stream,如果不是则抛出异常。

方法fetchEventSource

  1. 请求headers补上accept,值为text/event-stream
  2. 根据参数openWhenHidden,决定是否注册visibilitychange事件,用来实现页面隐藏后取消请求,页面重新显示后重新发送请求。这个机制可以借鉴
  3. 给参数signal注册abort事件,这样在外部调用AbortController的abort时,这里就能取消请求了,调用了dispose和resolve
  4. 调用create方法发送请求
    • 创建AbortController实例curRequestController,把它的signal传递给fetch,实现取消请求的功能
    • 使用fetch发送请求
    • 回调onopen
    • 读取响应流,直到结束,读流
    • 回调onclose
    • 调用dispose和resolve

3. parse.ts

这个文件提供了对返回的流进行解析的函数。

  • getBytes
    调用ReadableStream实例的getReader方法获得ReadableStreamDefaultReader实例,然后循环调用ReadableStreamDefaultReader的read方法直到流结束,read方法返回的是ReadableStreamReadResult,它的value属性就是流的内容,是Unit8Array类型的。
  • getLines
    解析getBytes读取的Unit8Array对象,从中得到行。
  • getMessage
    解析getLines中得到的行,得到消息,遇到空行认为是一个消息的结束。

这里的解析过程让我想起了表达式的解析,有借鉴意义,同时这里对于高阶函数的运用让人眼前一亮!

连接关闭的问题

在实践过程中,我遇到了请求关闭的问题。从前端不论是原生的EventSource的close或是fetchEventSource的signal都能实现关闭请求。但是对于原生的EventSource却无法单独从后端关闭请求,我用的是node写的简单的后端,代码如下(在阮一峰老师的代码上改的)

const http = require('http');

const server = http.createServer((req, res) => {
  const fileName = `.${req.url}`;

  if (fileName === './stream') {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'Access-Control-Allow-Origin': '*',
    });

    res.write('retry: 10000\n');
    res.write(`data: ${new Date()}\n\n`);

    let n = 1;
    const interval = setInterval(() => {
      if (n > 10) {
        res.end();        
        return;
      }
      res.write(`data: ${new Date()}\n\n`);
      res.write('event: test\n');
      res.write('data: this is test msg\n\n');
      n++;
    }, 1000);

    req.connection.addListener('close', () => {
      clearInterval(interval);
      console.log('close...');
    }, false);
  }
});

server.listen(8811, '127.0.0.1');

我故意加了个计数,10次以后由服务器端关闭连接。结果是前端会停一会然后再重试。
以下是前端的代码:

<!DOCTYPE html>
<html>
  <head>
    <title>SSE</title>
  </head>
  <body>
    <div id="container">

    </div>
    <button id="openBtn">open</button><button id="closeBtn">close</button>
    <script>
      const $container = document.querySelector('#container');
      function addMessage(msg) {
        $container.innerHTML += msg +'<br>';
      }

      const $openBtn = document.querySelector('#openBtn');
      let source;
      $openBtn.addEventListener('click', () => {
        if (source) {
          return;
        }
        source = new EventSource('http://localhost:8811/stream');
        source.addEventListener('open', () => {
          addMessage('opened...');
        });
        source.addEventListener('message', (event) => {
          addMessage('[message] ' + event.data);
        });
        source.addEventListener('error', (error) => {
          console.error('error:', error);
        });
        source.addEventListener('test', (event) => {
          console.log(event);
          addMessage('[test] ' + event.data);
        });
      });

      const $closeBtn = document.querySelector('#closeBtn');
      $closeBtn.addEventListener('click', () => {
        if (!source) {
          return;
        }
        source.close();
        source = null;
        addMessage('close...');
      });
    </script>
  </body>
</html>

如果用@microsoft/fetch-event-source的话,主要在onclose不throw异常就不会再重试。

<!DOCTYPE html>
<html>
  <head>
    <title>@microsoft/fetch-event-source</title>
  </head>
  <body>
    <div id="container">

    </div>
    <button id="openBtn">open</button><button id="closeBtn">close</button>
    <script src="./lib/merge.js"></script>
    <script>
      const $container = document.querySelector('#container');
      function addMessage(msg) {
        $container.innerHTML += msg +'<br>';
      }

      const $openBtn = document.querySelector('#openBtn');
      let source;
      let abortController;
      $openBtn.addEventListener('click', () => {
        if (abortController) {
          return;
        }
        abortController = new AbortController();
        fetchEventSource('http://localhost:8811/stream', {
          signal: abortController.signal,
          async onopen(res) {
            addMessage('open...');
            console.log('open: ', res);
          },
          onmessage(msg) {
            addMessage(`[${msg.event}]: ${msg.data}`);
          },
          onclose() {
            addMessage('close...');
            abortController = null;         
          },
          onerror() {
            console.log('error...');
          }
        })
      });

      const $closeBtn = document.querySelector('#closeBtn');
      $closeBtn.addEventListener('click', () => {
        if (abortController) {
          abortController.abort();
          abortController = null;
        }
      });    
    </script>
  </body>
</html>

其中merge.js是我手动把@microsoft/fetch-event-source编译后合并到一个文件中的。
先说一下为什么@microsoft/fetch-event-source为啥跟EventSource的行为不同。
在这里插入图片描述
如果服务器端主动关闭了连接,getBytes这里会结束,然后往下执行onclose回调,如果onclose正常执行没有抛出异常的话,就会继续执行dispose和resolve,此时不会触发任何重试的机制。如果在onclose抛出了异常,就像@microsoft/fetch-event-source官方代码中说的trow new RetriableError(),那么就不会执行dispose和resolve(dispose中有curRequestController.abort调用)而是直接跳到底下的catch中,此时curRequestController.signal.aborted是false,因此就会触发重试机制。这里再说一下如果通过传递给fetchEventSource函数的signal来通知取消请求时,getBytes这里会直接抛出异常,根本走不到onclose而是到了底下的catch,此时由于在监控到fetchEventSource外部传入的signal的abort时间时调用了dispose,因此curRequestController.signal.aborted时true的,因此即使进入了catch也不会触发重试。
在这里插入图片描述
接下来说一下原生EventSource关闭连接的问题。前端通过调用EventSource实例的close肯定能关闭。但是后端主动关闭的话就会像上面说的过一段时间后重试。我没有办法再不修改前端的前提下让后端能关闭连接。我的想法是后端如果要关闭连接主动发一个事件给前端,让前端调用close方法关闭。但是这样又增加了前后端的耦合我其实是不太喜欢的,不过没办法,前后端本身就是耦合的。

关于自定义事件

原生EventSource和@microsoft/fetch-event-source还有一些不同,比如对自定义事件的处理和对注释的处理。
@microsoft/fetch-event-source所有事件都是通过onmessage处理的,通过type可以区分,默认的message事件type是空,自定义事件的消息type是有值的可以通过type来自行分发。
对于注释,原生EventSource是不会触发任何事件的,但是@microsoft/fetch-event-source已经回调onmessage,注释的type也是空,而且它的data也是空。

我把上面的示例代码打包放在这里了,需要的自取。

注:如果你觉得有帮助请关注点赞哈:)

### 微信小程序中集成和使用 `@microsoft/fetch-event-source` 库 #### 安装库文件 要在微信小程序环境中引入第三方 JavaScript 库,通常的做法是先通过 npm 或 yarn 将其下载至本地项目目录下。对于 `@microsoft/fetch-event-source` 这样的模块来说: ```bash npm install @microsoft/fetch-event-source --save ``` 此命令会把所需的包添加到项目的依赖列表里并保存下来。 #### 修改编译配置 由于微信小程序默认并不支持直接解析 ES6 模块语法以及 CommonJS 的 require() 方法调用外部 NPM 包,因此需要调整小程序的构建工具设置以便能够正确处理这些现代特性。具体操作是在根目录下的 `project.config.json` 文件内开启对 npm 支持选项[^2]。 #### 使用 fetchEventSource 函数发起 SSE 请求 当一切准备工作完成后,在页面逻辑层(通常是 .js 文件),可以通过如下方式创建事件源实例并向服务器发送请求获取实时更新的数据流: ```javascript import { fetchEventSource } from '@microsoft/fetch-event-source'; Page({ onLoad: function () { const param = {}; // 参数对象初始化 let eventSource = null; try { eventSource = fetchEventSource(this.data.apiUrl, { method: 'GET', headers: new Headers({ accept: 'text/event-stream' }), onmessage(msg) { console.log('New message:', msg); // 更新UI或其他业务逻辑... }, onopen(response) { console.info('Connection opened with status:', response.status); }, onclose(reason) { console.warn('Connection closed because of ', reason); } }); } catch (error) { console.error(error.message || error); } this.setData({eventSource}); }, onDestroy:function(){ if(this.data.eventSource){ this.data.eventSource.close(); } } }) ``` 上述代码片段展示了如何利用 `fetchEventSource()` 来建立与服务端之间的持久连接,并监听消息推送事件以实现实时交互功能[^1]。 需要注意的是,考虑到跨域资源共享(CORS)策略的影响,确保 API 接口地址允许从小程序域名发出此类请求是非常重要的;另外,出于性能考虑建议合理控制重连机制以免频繁尝试重建链接造成资源浪费。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值