SSE(Server-Sent Events)是一种服务端主动生成事件的 web 技术,它可以向客户端持续推送数据,而无需客户端轮询服务器,常常用于实现一些实时性较高的 web 应用场景,如聊天室、股票行情等。
在使用 SSE 的过程中,代码注释和错误处理是非常重要的,它们可以帮助我们更好地理解 SSE 的工作流程,提高代码的质量和稳定性,本文将从这两个方面来探讨。
代码注释
代码注释是一种辅助阅读的方式,可以让其他人更好地理解代码的作用和流程,对于复杂的 SSE 代码,注释更是不可或缺的。
EventSource 类的注释
SSE 的核心是 EventSource 类,它用于与服务器建立连接并接收服务端发送的事件,这里我们需要给 EventSource 类添加一些注释,方便其他人阅读和使用。
// www.javascriptcn.com code example
class EventSource {
constructor(url, options) {
// 建立连接的 URL
this.url = url;
// 是否使用 CORS(跨域资源共享)
this.withCredentials = options?.withCredentials || false;
// 最后一个 event ID,用于客户端与服务器同步事件状态
this.lastEventId = "";
// 事件回调函数列表
this.listeners = {};
// 重试时间间隔
this.retry = DEFAULT_RETRY_INTERVAL;
// 实际重试时间间隔
this.actualRetry = this.retry;
// 连接的关闭原因
this.closeReason = "";
// 建立连接
this._connect();
}
// 发送消息,SSE 不能进行双向通信,所以该方法没有实际作用
send() {
// do nothing
}
// 打开连接,请求服务端生成事件
_connect() {
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
xhr.open("GET", this.url, true);
xhr.withCredentials = this.withCredentials;
xhr.setRequestHeader("Cache-Control", "no-cache");
xhr.setRequestHeader("Accept", "text/event-stream");
xhr.setRequestHeader("Connection", "keep-alive");
xhr.setRequestHeader("Last-Event-ID", this.lastEventId);
// 监听 readyStateChange 事件,处理响应值
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.OPENED) {
// 连接已建立
this._dispatch("open", {});
} else if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
// 响应头已经接收
if (xhr.status === 200) {
// 连接成功
this.actualRetry = this.retry;
this._dispatch("connect", {});
} else {
// 连接失败,继续重试
this._retry();
}
} else if (xhr.readyState === XMLHttpRequest.LOADING) {
// 正在接收响应体
this._handleMessage(xhr.responseText);
} else if (xhr.readyState === XMLHttpRequest.DONE) {
// 连接已关闭
this._close(xhr.status, xhr.statusText);
}
};
// 监听 error 事件,处理连接错误
xhr.onerror = (err) => {
this._retry();
};
// 发送请求
xhr.send();
}
// 断开连接
close() {
if (this.readyState !== CLOSED) {
this.readyState = CLOSED;
if (this.xhr) {
this.xhr.abort();
}
this._dispatch("close", {});
this.removeAllListeners();
}
}
// 添加事件监听
addEventListener(type, listener) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(listener);
}
// 移除事件监听
removeEventListener(type, listener) {
if (!this.listeners[type]) {
return;
}
const index = this.listeners[type].indexOf(listener);
if (index >= 0) {
this.listeners[type].splice(index, 1);
}
}
// 移除所有事件监听
removeAllListeners() {
this.listeners = {};
}
// 触发事件,回调所有监听器
_dispatch(type, event) {
const listeners = this.listeners[type];
if (!listeners) {
return;
}
for (let i = 0; i < listeners.length; i++) {
listeners[i].call(this, event);
}
}
// 处理从服务端发送的消息
_handleMessage(msg) {
const lines = msg.split(/\r\n|\n|\r/);
let evt = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.length === 0) {
// 空行,代表一个完整的事件
evt && this._dispatchEvent(evt);
evt = null;
} else if (line.startsWith(":")) {
// 注释行,忽略
} else {
// 事件数据行
const idx = line.indexOf(":");
const field = idx >= 0 ? line.substring(0, idx) : line;
const value = idx >= 0 ? line.substring(idx + 1) : "";
if (field === "event") {
// 事件名称,赋值给 evt 对象
evt = { type: value };
} else if (field === "data") {
// 数据,添加到 evt 对象的 data 属性中
if (!evt.data) {
evt.data = "";
}
evt.data += "\n" + value;
} else if (field === "id") {
// 事件 ID,赋值给 lastEventId 属性
this.lastEventId = value;
} else if (field === "retry") {
// 事件重试时间,赋值给 retry 属性
this.actualRetry = parseInt(value);
if (isNaN(this.actualRetry)) {
this.actualRetry = this.retry;
}
}
}
}
}
// 触发事件,处理重试
_dispatchEvent(evt) {
evt.originalLastEventId = this.lastEventId;
this._dispatch(evt.type, evt);
if (evt.type !== "message") {
// 非 message 事件,同步 lastEventId 属性
this.lastEventId = evt.originalLastEventId;
}
if (this.readyState === CONNECTING) {
// 连接成功,更新 readyState 属性
this.readyState = OPEN;
}
}
// 重试连接
_retry() {
// 更新重试时间间隔
this.actualRetry *= 2;
if (this.actualRetry > MAX_RETRY_INTERVAL) {
this.actualRetry = MAX_RETRY_INTERVAL;
}
// 重试连接
setTimeout(() => {
this._connect();
}, this.actualRetry);
}
// 连接已关闭,更新 closeReason 属性,触发服务器关闭事件
_close(code, reason) {
this.readyState = CLOSED;
this.xhr = null;
this.closeReason = reason;
this._dispatch("server_close", {
code: code,
reason: reason,
});
this.removeAllListeners();
if (this.actualRetry !== this.retry) {
// 更新重试时间间隔
this.actualRetry = this.retry;
}
// 重试连接
setTimeout(() => {
this._connect();
}, this.actualRetry);
}
}事件回调函数的注释
SSE 的事件回调函数通常由我们自己编写,我们需要在回调函数中添加一些注释,用于描述该事件具体的作用和响应值。
例如,下面是一个事件回调函数的示例代码:
eventSource.addEventListener("open", (event) => {
// 连接成功,在控制台输出信息
console.log("Connection opened");
});我们可以对该事件的作用和响应值进行注释:
eventSource.addEventListener("open", (event) => {
// 连接成功
// event:事件对象,包含以下属性
// - type: 事件类型,值为 "open"
console.log("Connection opened");
});这样其他人在阅读代码时,就可以更好地了解这个事件回调函数的作用和响应值。
错误处理
SSE 使用 XMLHttpRequest 对象与服务器建立连接并传送数据,由于网络原因等因素,连接可能会出现错误。在错误处理中,我们需要对各种错误情况进行处理,保证 SSE 服务的稳定性。
请求错误处理
当请求失败时,我们需要进行重试操作。
xhr.onerror = (err) => {
this._retry();
};这里的 _retry 方法会根据实际情况更新重试时间间隔,并在一定时间后重试连接。
连接关闭处理
当连接被关闭时,我们需要更新状态信息,并触发 server_close 事件。
// www.javascriptcn.com code example
_close(code, reason) {
this.readyState = CLOSED;
this.xhr = null;
this.closeReason = reason;
this._dispatch("server_close", {
code: code,
reason: reason,
});
// 其他代码
}服务端错误处理
服务端错误可能会导致连接中断或者返回错误信息,我们可以通过错误码来判断是否是服务端错误,并添加对应的错误处理。
// www.javascriptcn.com code example
if (xhr.status === 200) {
// 连接成功
this.actualRetry = this.retry;
this._dispatch("connect", {});
} else {
// 连接失败,继续重试
if (xhr.status >= 500 && xhr.status < 600) {
// 服务端错误,打印错误信息
console.error(`Server Error[${xhr.status}]: ${xhr.statusText}`);
}
this._retry();
}这里的错误信息会在控制台上输出,方便我们查找错误原因。
总结
代码注释和错误处理是 SSE 服务开发过程中必不可少的一部分,它们可以提高代码的可读性和健壮性,避免出现错误和 bug,保证服务的稳定性和性能。我们需要在开发过程中养成良好的注释和错误处理习惯,这对我们的职业发展和工作效率都有很大的帮助。
Source: FunTeaLearn,Please indicate the source for reprints https://funteas.com/post/64a919e848841e98945666ea