📝逐行拆解 event-source-polyfill 源码:理解 SSE polyfill 的设计思路

前言

Hello~大家好。我是秋天的一阵风

一、原生Event Source的局限性

在前端开发中,Server-Sent Events(SSE)是实现服务器向客户端单向推送消息的重要技术。但原生 EventSource 在部分场景下存在局限,比如携带 token 进行身份验证时不够灵,还有部分低版本浏览器(如 IE 全版本、早期 Safari)不支持该 API。而 event-source-polyfill 作为优秀的兼容库,很好地解决了这些问题。

本文将基于event-source.js 源码,深入剖析这个开源项目的代码架构、重要文件逻辑以及核心实现,带你揭开它能携带 token 请求 SSE 的奥秘

关于 event-source-polyfill 的具体使用方法(包括安装步骤、API 调用、参数配置等详细内容),本文不再赘述。读者可参考官方项目文档获取完整指南:
github.com/Yaffle/Even...

二、项目架构概览

event-source.js 源码来看,整个项目采用模块化的自执行函数结构,将所有功能封装在一个全局函数中,避免了对全局环境的污染。其核心架构围绕 SSE 通信的完整生命周期展开,包括环境兼容性处理、核心类定义、连接建立与维护、消息解析、错误处理和重连机制等部分。

代码首先进行全局环境变量的获取和兼容性处理,为后续功能实现奠定基础。

接着定义了一系列核心类,如 XHRWrapperHeadersPolyfill、各类 Transport 类、EventTarget 以及事件类等,这些类各司其职,共同支撑起 SSE 通信的各项功能。

最后通过 EventSourcePolyfill 类对外提供统一的接口,实现了与原生 EventSource 相似的使用方式,同时扩展了更多实用功能。

三、重要代码逻辑解析

1. 环境兼容性处理

源码开篇就对不同浏览器环境进行了兼容处理,这是确保库在各种环境下正常运行的基础。

js 复制代码
  var setTimeout = global.setTimeout;
  var clearTimeout = global.clearTimeout;
  var XMLHttpRequest = global.XMLHttpRequest;
  var XDomainRequest = global.XDomainRequest;
  var ActiveXObject = global.ActiveXObject;
  var NativeEventSource = global.EventSource;

  var document = global.document;
  var Promise = global.Promise;
  var fetch = global.fetch;
  var Response = global.Response;
  var TextDecoder = global.TextDecoder;
  var TextEncoder = global.TextEncoder;
  var AbortController = global.AbortController;

  if (typeof window !== "undefined" && typeof document !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2
    document.readyState = "loading";
    window.addEventListener("load", function (event) {
      document.readyState = "complete";
    }, false);
  }

  if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6
    XMLHttpRequest = function () {
      return new ActiveXObject("Microsoft.XMLHTTP");
    };
  }

  if (Object.create == undefined) {
    Object.create = function (C) {
      function F(){}
      F.prototype = C;
      return new F();
    };
  }

  if (!Date.now) {
    Date.now = function now() {
      return new Date().getTime();
    };
  }

  // see #118 (Promise#finally with polyfilled Promise)
  // see #123 (data URLs crash Edge)
  // see #125 (CSP violations)
  // see pull/#138
  // => No way to polyfill Promise#finally

  if (AbortController == undefined) {
    var originalFetch2 = fetch;
    fetch = function (url, options) {
      var signal = options.signal;
      return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) {
        var reader = response.body.getReader();
        signal._reader = reader;
        if (signal._aborted) {
          signal._reader.cancel();
        }
        return {
          status: response.status,
          statusText: response.statusText,
          headers: response.headers,
          body: {
            getReader: function () {
              return reader;
            }
          }
        };
      });
    };
    AbortController = function () {
      this.signal = {
        _reader: null,
        _aborted: false
      };
      this.abort = function () {
        if (this.signal._reader != null) {
          this.signal._reader.cancel();
        }
        this.signal._aborted = true;
      };
    };
  }

  function TextDecoderPolyfill() {
    this.bitsNeeded = 0;
    this.codePoint = 0;
  }

  TextDecoderPolyfill.prototype.decode = function (octets) {
    function valid(codePoint, shift, octetsCount) {
      if (octetsCount === 1) {
        return codePoint >= 0x0080 >> shift && codePoint << shift <= 0x07FF;
      }
      if (octetsCount === 2) {
        return codePoint >= 0x0800 >> shift && codePoint << shift <= 0xD7FF || codePoint >= 0xE000 >> shift && codePoint << shift <= 0xFFFF;
      }
      if (octetsCount === 3) {
        return codePoint >= 0x010000 >> shift && codePoint << shift <= 0x10FFFF;
      }
      throw new Error();
    }
    function octetsCount(bitsNeeded, codePoint) {
      if (bitsNeeded === 6 * 1) {
        return codePoint >> 6 > 15 ? 3 : codePoint > 31 ? 2 : 1;
      }
      if (bitsNeeded === 6 * 2) {
        return codePoint > 15 ? 3 : 2;
      }
      if (bitsNeeded === 6 * 3) {
        return 3;
      }
      throw new Error();
    }
    var REPLACER = 0xFFFD;
    var string = "";
    var bitsNeeded = this.bitsNeeded;
    var codePoint = this.codePoint;
    for (var i = 0; i < octets.length; i += 1) {
      var octet = octets[i];
      if (bitsNeeded !== 0) {
        if (octet < 128 || octet > 191 || !valid(codePoint << 6 | octet & 63, bitsNeeded - 6, octetsCount(bitsNeeded, codePoint))) {
          bitsNeeded = 0;
          codePoint = REPLACER;
          string += String.fromCharCode(codePoint);
        }
      }
      if (bitsNeeded === 0) {
        if (octet >= 0 && octet <= 127) {
          bitsNeeded = 0;
          codePoint = octet;
        } else if (octet >= 192 && octet <= 223) {
          bitsNeeded = 6 * 1;
          codePoint = octet & 31;
        } else if (octet >= 224 && octet <= 239) {
          bitsNeeded = 6 * 2;
          codePoint = octet & 15;
        } else if (octet >= 240 && octet <= 247) {
          bitsNeeded = 6 * 3;
          codePoint = octet & 7;
        } else {
          bitsNeeded = 0;
          codePoint = REPLACER;
        }
        if (bitsNeeded !== 0 && !valid(codePoint, bitsNeeded, octetsCount(bitsNeeded, codePoint))) {
          bitsNeeded = 0;
          codePoint = REPLACER;
        }
      } else {
        bitsNeeded -= 6;
        codePoint = codePoint << 6 | octet & 63;
      }
      if (bitsNeeded === 0) {
        if (codePoint <= 0xFFFF) {
          string += String.fromCharCode(codePoint);
        } else {
          string += String.fromCharCode(0xD800 + (codePoint - 0xFFFF - 1 >> 10));
          string += String.fromCharCode(0xDC00 + (codePoint - 0xFFFF - 1 & 0x3FF));
        }
      }
    }
    this.bitsNeeded = bitsNeeded;
    this.codePoint = codePoint;
    return string;
  };

  // Firefox < 38 throws an error with stream option
  var supportsStreamOption = function () {
    try {
      return new TextDecoder().decode(new TextEncoder().encode("test"), {stream: true}) === "test";
    } catch (error) {
      console.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error);
    }
    return false;
  };

  // IE, Edge
  if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) {
    TextDecoder = TextDecoderPolyfill;
  }

  var k = function () {
  };
  • 核心 API 适配 :获取全局对象中的 setTimeoutclearTimeoutXMLHttpRequest 等关键 API,针对不同浏览器的特性进行适配。例如,对于不支持 XMLHttpRequest 的环境,尝试使用ActiveXObject创建对象。

  • 基础功能补全 :为不支持 Object.createDate.now 等方法的环境添加相应实现,保证代码后续逻辑能正常执行。

  • 高级 API polyfill :对 AbortControllerTextDecoder 等 API 进行模拟实现。比如当环境中没有 AbortController 时,自定义一个包含信号和中止方法的类,确保请求中止功能正常工作。

2. XHRWrapper 类

js 复制代码
 function XHRWrapper(xhr) {
    this.withCredentials = false;
    this.readyState = 0;
    this.status = 0;
    this.statusText = "";
    this.responseText = "";
    this.onprogress = k;
    this.onload = k;
    this.onerror = k;
    this.onreadystatechange = k;
    this._contentType = "";
    this._xhr = xhr;
    this._sendTimeout = 0;
    this._abort = k;
  }

  XHRWrapper.prototype.open = function (method, url) {
    this._abort(true);

    var that = this;
    var xhr = this._xhr;
    var state = 1;
    var timeout = 0;

    this._abort = function (silent) {
      if (that._sendTimeout !== 0) {
        clearTimeout(that._sendTimeout);
        that._sendTimeout = 0;
      }
      if (state === 1 || state === 2 || state === 3) {
        state = 4;
        xhr.onload = k;
        xhr.onerror = k;
        xhr.onabort = k;
        xhr.onprogress = k;
        xhr.onreadystatechange = k;
        // IE 8 - 9: XDomainRequest#abort() does not fire any event
        // Opera < 10: XMLHttpRequest#abort() does not fire any event
        xhr.abort();
        if (timeout !== 0) {
          clearTimeout(timeout);
          timeout = 0;
        }
        if (!silent) {
          that.readyState = 4;
          that.onabort(null);
          that.onreadystatechange();
        }
      }
      state = 0;
    };

    var onStart = function () {
      if (state === 1) {
        //state = 2;
        var status = 0;
        var statusText = "";
        var contentType = undefined;
        if (!("contentType" in xhr)) {
          try {
            status = xhr.status;
            statusText = xhr.statusText;
            contentType = xhr.getResponseHeader("Content-Type");
          } catch (error) {
            // IE < 10 throws exception for `xhr.status` when xhr.readyState === 2 || xhr.readyState === 3
            // Opera < 11 throws exception for `xhr.status` when xhr.readyState === 2
            // https://bugs.webkit.org/show_bug.cgi?id=29121
            status = 0;
            statusText = "";
            contentType = undefined;
            // Firefox < 14, Chrome ?, Safari ?
            // https://bugs.webkit.org/show_bug.cgi?id=29658
            // https://bugs.webkit.org/show_bug.cgi?id=77854
          }
        } else {
          status = 200;
          statusText = "OK";
          contentType = xhr.contentType;
        }
        if (status !== 0) {
          state = 2;
          that.readyState = 2;
          that.status = status;
          that.statusText = statusText;
          that._contentType = contentType;
          that.onreadystatechange();
        }
      }
    };
    var onProgress = function () {
      onStart();
      if (state === 2 || state === 3) {
        state = 3;
        var responseText = "";
        try {
          responseText = xhr.responseText;
        } catch (error) {
          // IE 8 - 9 with XMLHttpRequest
        }
        that.readyState = 3;
        that.responseText = responseText;
        that.onprogress();
      }
    };
    var onFinish = function (type, event) {
      if (event == null || event.preventDefault == null) {
        event = {
          preventDefault: k
        };
      }
      // Firefox 52 fires "readystatechange" (xhr.readyState === 4) without final "readystatechange" (xhr.readyState === 3)
      // IE 8 fires "onload" without "onprogress"
      onProgress();
      if (state === 1 || state === 2 || state === 3) {
        state = 4;
        if (timeout !== 0) {
          clearTimeout(timeout);
          timeout = 0;
        }
        that.readyState = 4;
        if (type === "load") {
          that.onload(event);
        } else if (type === "error") {
          that.onerror(event);
        } else if (type === "abort") {
          that.onabort(event);
        } else {
          throw new TypeError();
        }
        that.onreadystatechange();
      }
    };
    var onReadyStateChange = function (event) {
      if (xhr != undefined) { // Opera 12
        if (xhr.readyState === 4) {
          if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) {
            onFinish(xhr.responseText === "" ? "error" : "load", event);
          }
        } else if (xhr.readyState === 3) {
          if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11
            // and in Firefox 3.6
            onProgress();
          }
        } else if (xhr.readyState === 2) {
          onStart();
        }
      }
    };
    var onTimeout = function () {
      timeout = setTimeout(function () {
        onTimeout();
      }, 500);
      if (xhr.readyState === 3) {
        onProgress();
      }
    };

    // XDomainRequest#abort removes onprogress, onerror, onload
    if ("onload" in xhr) {
      xhr.onload = function (event) {
        onFinish("load", event);
      };
    }
    if ("onerror" in xhr) {
      xhr.onerror = function (event) {
        onFinish("error", event);
      };
    }
    // improper fix to match Firefox behaviour, but it is better than just ignore abort
    // see https://bugzilla.mozilla.org/show_bug.cgi?id=768596
    // https://bugzilla.mozilla.org/show_bug.cgi?id=880200
    // https://code.google.com/p/chromium/issues/detail?id=153570
    // IE 8 fires "onload" without "onprogress
    if ("onabort" in xhr) {
      xhr.onabort = function (event) {
        onFinish("abort", event);
      };
    }

    if ("onprogress" in xhr) {
      xhr.onprogress = onProgress;
    }

    // IE 8 - 9 (XMLHTTPRequest)
    // Opera < 12
    // Firefox < 3.5
    // Firefox 3.5 - 3.6 - ? < 9.0
    // onprogress is not fired sometimes or delayed
    // see also #64 (significant lag in IE 11)
    if ("onreadystatechange" in xhr) {
      xhr.onreadystatechange = function (event) {
        onReadyStateChange(event);
      };
    }

    if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) {
      url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true";
    }
    xhr.open(method, url, true);

    if ("readyState" in xhr) {
      // workaround for Opera 12 issue with "progress" events
      // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?)
      timeout = setTimeout(function () {
        onTimeout();
      }, 0);
    }
  };
  XHRWrapper.prototype.abort = function () {
    this._abort(false);
  };
  XHRWrapper.prototype.getResponseHeader = function (name) {
    return this._contentType;
  };
  XHRWrapper.prototype.setRequestHeader = function (name, value) {
    var xhr = this._xhr;
    if ("setRequestHeader" in xhr) {
      xhr.setRequestHeader(name, value);
    }
  };
  XHRWrapper.prototype.getAllResponseHeaders = function () {
    // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28
    return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : "";
  };
  XHRWrapper.prototype.send = function () {
    // loading indicator in Safari < ? (6), Chrome < 14, Firefox
    // https://bugzilla.mozilla.org/show_bug.cgi?id=736723
    if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" in XMLHttpRequest.prototype))) &&
        document != undefined &&
        document.readyState != undefined &&
        document.readyState !== "complete") {
      var that = this;
      that._sendTimeout = setTimeout(function () {
        that._sendTimeout = 0;
        that.send();
      }, 4);
      return;
    }

    var xhr = this._xhr;
    // withCredentials should be set after "open" for Safari and Chrome (< 19 ?)
    if ("withCredentials" in xhr) {
      xhr.withCredentials = this.withCredentials;
    }
    try {
      // xhr.send(); throws "Not enough arguments" in Firefox 3.0
      xhr.send(undefined);
    } catch (error1) {
      // Safari 5.1.7, Opera 12
      throw error1;
    }

该类对 XMLHttpRequestXDomainRequest 进行了封装,统一了接口,处理了不同浏览器间的差异,是实现 HTTP 请求的重要工具。

  • 请求管理 :提供 openabortsetRequestHeader 等方法,封装了底层 XHR 对象的操作。在 open 方法中,通过设置各种事件回调(``onloadonerroronprogress` 等),实现了对请求过程的全面监控。

  • 状态维护 :内部维护了请求的 readyStatestatus 等状态,确保在不同浏览器环境下能准确反映请求的当前状态。

  • 超时与中止处理:在 abort 方法中,清除超时定时器,终止底层 XHR 对象的请求,并触发相应的事件,保证资源能被正确释放。

3. Transport 类(XHRTransport 和 FetchTransport)

这两个类定义了数据传输的方式,分别基于 XMLHttpRequestfetch API,为连接建立提供了底层支持。

  • XHRTransport :基于 XMLHttpRequest 实现数据传输。在 open 方法中,配置请求头、设置 withCredentials 属性,发送GET请求,并通过各种事件回调与上层逻辑交互。当接收到数据时,通过 onProgressCallback 将数据片段传递出去。
js 复制代码
XHRTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
  xhr.open("GET", url);
  var offset = 0;
  xhr.onprogress = function () {
    var responseText = xhr.responseText;
    var chunk = responseText.slice(offset);
    offset += chunk.length;
    onProgressCallback(chunk);
  };
  // 其他事件回调设置...
};
  • FetchTransport :基于 fetch API 实现,适用于支持 fetch 的现代浏览器。通过AbortController实现请求的中止,使用 TextDecoder 解析二进制数据为文本,同样通过回调函数与上层交互。
js 复制代码
  FetchTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
    var reader = null;
    var controller = new AbortController();
    var signal = controller.signal;
    var textDecoder = new TextDecoder();
    fetch(url, {
      headers: headers,
      credentials: withCredentials ? "include" : "same-origin",
      signal: signal,
      cache: "no-store"
    }).then(function (response) {
      reader = response.body.getReader();
      onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers));
      // see https://github.com/promises-aplus/promises-spec/issues/179
      return new Promise(function (resolve, reject) {
        var readNextChunk = function () {
        };
        readNextChunk();
      });
    })["catch"](function (error) {
      if (error.name === "AbortError") {
        return undefined;
      } else {
        return error;
      }
    }).then(function (error) {
      onFinishCallback(error);
    });
    return {
      abort: function () {
        if (reader != null) {
          reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815
        }
        controller.abort();
      }
    };
  };

4. EventTarget 类及事件类

js 复制代码
  function EventTarget() {
    this._listeners = Object.create(null);
  }

  function throwError(e) {
    setTimeout(function () {
      throw e;
    }, 0);
  }

  EventTarget.prototype.dispatchEvent = function (event) {
    event.target = this;
    var typeListeners = this._listeners[event.type];
    if (typeListeners != undefined) {
      var length = typeListeners.length;
      for (var i = 0; i < length; i += 1) {
        var listener = typeListeners[i];
        try {
          if (typeof listener.handleEvent === "function") {
            listener.handleEvent(event);
          } else {
            listener.call(this, event);
          }
        } catch (e) {
          throwError(e);
        }
      }
    }
  };
  EventTarget.prototype.addEventListener = function (type, listener) {
    type = String(type);
    var listeners = this._listeners;
    var typeListeners = listeners[type];
    if (typeListeners == undefined) {
      typeListeners = [];
      listeners[type] = typeListeners;
    }
    var found = false;
    for (var i = 0; i < typeListeners.length; i += 1) {
      if (typeListeners[i] === listener) {
        found = true;
      }
    }
    if (!found) {
      typeListeners.push(listener);
    }
  };
  EventTarget.prototype.removeEventListener = function (type, listener) {
    type = String(type);
    var listeners = this._listeners;
    var typeListeners = listeners[type];
    if (typeListeners != undefined) {
      var filtered = [];
      for (var i = 0; i < typeListeners.length; i += 1) {
        if (typeListeners[i] !== listener) {
          filtered.push(typeListeners[i]);
        }
      }
      if (filtered.length === 0) {
        delete listeners[type];
      } else {
        listeners[type] = filtered;
      }
    }
  };

EventTarget 类实现了事件监听的核心功能,包括 addEventListenerremoveEventListenerdispatchEvent 方法, EventSourcePolyfill 类的父类,为事件驱动的编程模式提供了支持。

各类事件类(MessageEvent、ConnectionEvent、ErrorEvent 等)则封装了不同类型的事件数据,在特定场景下被触发,将相关信息传递给事件监听器。例如,MessageEvent 包含了服务器发送的消息数据和 lastEventId,当解析到完整消息时被触发。

5. EventSourcePolyfill 类

这是整个库的核心类,对外提供了与原生 EventSource 一致的接口,同时扩展了携带 token 等功能。

  • 初始化流程:在构造函数中,继承 EventTarget 的功能,初始化事件回调和属性,然后调用 start 函数启动核心流程。

  • 连接管理:通过 start 函数初始化参数、设置状态,调用 onTimeout 函数开始连接建立过程。在连接建立过程中,处理 URL、准备请求头(包括携带的 token 等信息),通过 Transport 类发送请求。

  • 消息处理:当接收到服务器发送的数据时,通过 onProgress 函数进行解析。使用状态机(FIELD_START、FIELD、VALUE_START 等状态)处理 SSE 格式的消息,解析出数据、事件类型、ID 等信息,当解析到完整消息时触发相应事件。

  • 关闭与重连:提供 close 方法用于主动关闭连接,清理资源。当连接发生错误时,通过 onFinish 函数处理,计算重连时间,安排下一次重连,实现自动重连机制。

6. 携带 token 的实现逻辑

在使用 event-source-polyfill 时,我们可以通过 options.headers 传递 token 等认证信息,这一功能的实现主要涉及请求头的设置过程。

在连接建立的 onTimeout 函数中,代码会准备请求头信息。首先设置默认的 Accep 头为 "text/event-stream",然后遍历用户传入的 headers 选项,将自定义的请求头(包括 Authorization 等携带 token 的头信息)添加到请求头集合中。

js 复制代码
var requestHeaders = {};
requestHeaders["Accept"] = "text/event-stream";
var headers = es.headers;
if (headers != undefined) {
  for (var name in headers) {
    if (Object.prototype.hasOwnProperty.call(headers, name)) {
      requestHeaders[name] = headers[name];
    }
  }
}

之后,在 Transport 类的open方法中,这些请求头会被设置到实际的请求对象(XMLHttpRequestfetch 的 options)中,从而实现了在 SSE 请求中携带 token 的功能。

四、核心流程执行顺序

1. 初始化阶段

自执行函数是整个代码的入口,在全局环境中首先执行,完成一系列初始化操作。

js 复制代码
(function (global) {
  "use strict";
  // 获取全局环境中的关键对象和API
  var setTimeout = global.setTimeout;
  var clearTimeout = global.clearTimeout;
  var XMLHttpRequest = global.XMLHttpRequest;
  var XDomainRequest = global.XDomainRequest;
  var ActiveXObject = global.ActiveXObject;
  var NativeEventSource = global.EventSource;
  // ... 其他变量获取
  // 兼容性处理
  if (typeof window !== "undefined" && typeof document !== "undefined" && !("readyState" in document) && document.body == null) { 
    document.readyState = "loading";
    window.addEventListener("load", function (event) {
      document.readyState = "complete";
    }, false);
  }
  // ... 其他兼容性处理代码
  // 定义核心类
  function XHRWrapper(xhr) { ... }
  function HeadersPolyfill(all) { ... }
  function XHRTransport() { ... }
  function FetchTransport() { ... }
  function EventTarget() { ... }
  // ... 其他类定义
  // 暴露接口
  (function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
      var v = factory(exports);
      if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
      define(["exports"], factory);
    }
    else {
      factory(global);
    }
  })(function (exports) {
    exports.EventSourcePolyfill = EventSourcePolyfill;
    exports.NativeEventSource = NativeEventSource;
    exports.EventSource = R;
  });
})(...);

这一阶段先获取全局对象中的各类 API,如 XMLHttpRequestsetTimeout 等,然后针对不同浏览器环境进行兼容性处理,比如为低版本浏览器补全 Object.createDate.now 等方法,对 AbortControllerTextDecoder 等 API 进行 polyfill。接着定义了 XHRWrapperTransport 类、EventTarget 等核心类,最后通过工厂函数将相关接口暴露出去,供外部使用。

2. 实例化阶段

当开发者创建 EventSource 实例时,实际会初始化 EventSourcePolyfill 类。

js 复制代码
function EventSourcePolyfill(url, options) {
  EventTarget.call(this);
  options = options || {};
  this.onopen = undefined;
  this.onmessage = undefined;
  this.onerror = undefined;
  this.url = undefined;
  this.readyState = undefined;
  this.withCredentials = undefined;
  this.headers = undefined;
  this._close = undefined;
  start(this, url, options);
}

在构造函数中,首先调用 EventTarget 的构造函数以继承事件相关功能,然后初始化 onopenonmessage 等事件回调属性,以及 urlreadyState 等实例属性。最后调用 start 函数,启动 SSE 通信的核心流程。

3. 连接建立阶段

  • start 函数是连接建立的关键,会完成参数初始化并启动连接过程。
js 复制代码
function start(es, url, options) {
  url = String(url);
  var withCredentials = Boolean(options.withCredentials);
  var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId";
  var initialRetry = clampDuration(1000);
  var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000);
  // ... 其他参数和状态初始化
  var onTimeout = function () {
    // 连接建立具体逻辑
  };
  es.url = url;
  es.readyState = CONNECTING;
  es.withCredentials = withCredentials;
  es.headers = headers;
  es._close = close;
  onTimeout();
}

start 函数先解析并处理传入的 urloptions 参数,设置初始重试时间、心跳超时时间等。然后定义 onTimeout 等回调函数,初始化 EventSourcePolyfill 实例的属性,最后调用 onTimeout 函数开始建立连接。

  • onTimeout 函数负责处理 URL、准备请求头并发送请求:
js 复制代码
var onTimeout = function () {
  timeout = 0;
  if (currentState !== WAITING) {
    // 心跳和超时处理
    return;
  }
  wasActivity = false;
  textLength = 0;
  timeout = setTimeout(function () {
    onTimeout();
  }, heartbeatTimeout);
  currentState = CONNECTING;
  // ... 缓冲区和状态重置
  var requestURL = url;
  if (url.slice(0, 5) !== "data:" && url.slice(0, 5) !== "blob:") {
    if (lastEventId !== "") {
      // URL处理逻辑,添加lastEventId参数
    }
  }
  var requestHeaders = {};
  requestHeaders["Accept"] = "text/event-stream";
  var headers = es.headers;
  if (headers != undefined) {
    for (var name in headers) {
      if (Object.prototype.hasOwnProperty.call(headers, name)) {
        requestHeaders[name] = headers[name];
      }
    }
  }
  try {
    abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders);
  } catch (error) {
    close();
    throw error;
  }
};

onTimeout 函数中,先检查状态,若不是等待状态则进行心跳相关处理。否则重置相关状态和缓冲区,处理 URL(添加 lastEventId 参数),准备请求头(包括用户传入的 token 等信息),最后通过 transport.open 方法发送请求,建立与服务器的连接。

4. 消息接收与解析阶段

当服务器发送数据时,会触发 onProgress 回调函数进行消息解析。

js 复制代码
var onProgress = function (textChunk) {
  if (currentState === OPEN) {
    var n = -1;
    for (var i = 0; i < textChunk.length; i += 1) {
      var c = textChunk.charCodeAt(i);
      if (c === "\n".charCodeAt(0) || c === "\r".charCodeAt(0)) {
        n = i;
      }
    }
    var chunk = (n !== -1 ? textBuffer : "") + textChunk.slice(0, n + 1);
    textBuffer = (n === -1 ? textBuffer : "") + textChunk.slice(n + 1);
    if (textChunk !== "") {
      wasActivity = Date.now();
      textLength += textChunk.length;
    }
    for (var position = 0; position < chunk.length; position += 1) {
      var c = chunk.charCodeAt(position);
      if (state === AFTER_CR && c === "\n".charCodeAt(0)) {
        state = FIELD_START;
      } else {
        if (state === AFTER_CR) {
          state = FIELD_START;
        }
        if (c === "\r".charCodeAt(0) || c === "\n".charCodeAt(0)) {
          if (state !== FIELD_START) {
            // 解析字段和值
            if (state === FIELD) {
              valueStart = position + 1;
            }
            var field = chunk.slice(fieldStart, valueStart - 1);
            var value = chunk.slice(valueStart + (valueStart < position && chunk.charCodeAt(valueStart) === " ".charCodeAt(0) ? 1 : 0), position);
            // 根据字段类型处理数据
            if (field === "data") {
              dataBuffer += "\n";
              dataBuffer += value;
            } else if (field === "id") {
              lastEventIdBuffer = value;
            } else if (field === "event") {
              eventTypeBuffer = value;
            } else if (field === "retry") {
              initialRetry = parseDuration(value, initialRetry);
              retry = initialRetry;
            } else if (field === "heartbeatTimeout") {
              heartbeatTimeout = parseDuration(value, heartbeatTimeout);
              // 处理心跳超时
            }
          }
          if (state === FIELD_START) {
            if (dataBuffer !== "") {
              // 触发事件
              lastEventId = lastEventIdBuffer;
              if (eventTypeBuffer === "") {
                eventTypeBuffer = "message";
              }
              var event = new MessageEvent(eventTypeBuffer, {
                data: dataBuffer.slice(1),
                lastEventId: lastEventIdBuffer
              });
              es.dispatchEvent(event);
              // 调用事件回调
              if (eventTypeBuffer === "open") {
                fire(es, es.onopen, event);
              } else if (eventTypeBuffer === "message") {
                fire(es, es.onmessage, event);
              } else if (eventTypeBuffer === "error") {
                fire(es, es.onerror, event);
              }
              if (currentState === CLOSED) {
                return;
              }
            }
            // 重置缓冲区
            dataBuffer = "";
            eventTypeBuffer = "";
          }
          state = c === "\r".charCodeAt(0) ? AFTER_CR : FIELD_START;
        } else {
          // 状态机状态转换
          if (state === FIELD_START) {
            fieldStart = position;
            state = FIELD;
          }
          if (state === FIELD) {
            if (c === ":".charCodeAt(0)) {
              valueStart = position + 1;
              state = VALUE_START;
            }
          } else if (state === VALUE_START) {
            state = VALUE;
          }
        }
      }
    }
  }
};

onProgress 函数先对接收的文本进行分片处理,然后使用状态机(AFTER_CR、FIELD_START、FIELD 等状态)解析 SSE 格式的消息。根据不同的字符(换行符、冒号等)转换状态,提取字段(data、id、event 等)和对应的值,当解析到完整消息(遇到空行)时,构造 MessageEvent 并触发,同时调用相应的事件回调函数,最后重置缓冲区准备解析下一条消息。

5. 错误处理与重连阶段

当连接发生错误时,onFinish 函数会处理错误并安排重连。

js 复制代码
var onFinish = function (error) {
  if (currentState === OPEN || currentState === CONNECTING) {
    currentState = WAITING;
    if (timeout !== 0) {
      clearTimeout(timeout);
      timeout = 0;
    }</doubaocanvas>

当连接出现错误(如网络中断、服务器返回非 200 状态码等)时,onFinish 函数被触发。

首先将当前状态更新为 WAITING,清除已有的超时定时器,避免不必要的时间触发。然后根据当前的重试时间(retry)设置新的定时器,定时器到期后会调用 onTimeout 函数进行重连。

同时,重试时间会按照指数退避策略进行更新,即每次重连失败后,重试时间会翻倍,但不会超过初始重试时间的 16 倍,也不会超出最大重连时间限制,这样既保证了重连的可能性,又避免了过于频繁的重连对服务器造成过大压力。

接着,将 EventSource 实例的 readyState 设置为 CONNECTING,表明正在尝试重连。

然后创建一个 ErrorEvent 事件,将错误信息封装其中,并通过 dispatchEvent 方法触发该事件,同时调用实例的 onerror 回调函数,让开发者能够感知到错误的发生。如果错误信息存在,还会将其打印到控制台,方便调试。

6. 关闭阶段

关闭阶段可分为主动关闭和被动关闭两种情况,主动关闭是开发者调用 close 方法,被动关闭可能是由于连接发生严重错误等原因。

js 复制代码
EventSourcePolyfill.prototype.close = function () {
  this._close();
};
var close = function () {
  currentState = CLOSED;
  if (abortController != undefined) {
    abortController.abort();
    abortController = undefined;
  }
  if (timeout !== 0) {
    clearTimeout(timeout);
    timeout = 0;
  }
  es.readyState = CLOSED;
};

当调用 EventSourcePolyfill 实例的 close 方法时,实际上是调用了内部的 close 函数。

在 close 函数中,首先将当前状态设置为 CLOSED,表明连接已关闭。

如果存在 abortController(用于中止请求的控制器),则调用其 abort 方法中止当前的请求,并将 abortController 设置为 undefined,释放资源。同时,清除所有的超时定时器,避免定时器到期后触发不必要的重连等操作。最后,将实例的 readyState 更新为 CLOSED,让外部能够通过该属性知晓连接的状态。

总结

通过以上各个步骤的分析和探究,我们终于知道了 event-source-polyfill 是如何完整地实现了 SSE 通信的生命周期管理,从初始化到连接建立、消息处理、错误重连再到最终关闭,每个阶段都有清晰的逻辑和具体的代码实现,确保了在各种环境下都能稳定、可靠地进行服务器推送消息的接收与处理,尤其是携带 token 进行身份验证的功能,极大地扩展了 SSE 在实际开发中的应用场景。

相关推荐
chxii16 分钟前
6.3Element UI 的表单
javascript·vue.js·elementui
张努力17 分钟前
从零开始的开发一个vite插件:一个程序员的"意外"之旅 🚀
前端·vue.js
远帆L17 分钟前
前端批量导入内容——word模板方案实现
前端
Codebee22 分钟前
OneCode3.0-RAD 可视化设计器 配置手册
前端·低代码
chxii36 分钟前
6.4 Element UI 中的 <el-table> 表格组件
vue.js·ui·elementui
葡萄城技术团队37 分钟前
【SpreadJS V18.2 新版本】设计器新特性:四大主题方案,助力 UI 个性化与品牌适配
前端
lumi.1 小时前
Swiper属性全解析:快速掌握滑块视图核心配置!(2.3补充细节,详细文档在uniapp官网)
前端·javascript·css·小程序·uni-app
调皮LE1 小时前
可放大缩小弹窗组件,基于element-ui的vue2版本
前端
陈随易1 小时前
10年老前端,分享20+严选技术栈
前端·后端·程序员
我的小月月1 小时前
基于Canvas实现的网页取色器功能解析
前端