feat: 添加自动重连机制和重连配置选项到 RemoteApp 类,并更新相关文档

This commit is contained in:
2026-02-05 14:09:14 +08:00
parent a46510949b
commit 8fc9605242
5 changed files with 418 additions and 42 deletions

View File

@@ -17,7 +17,6 @@ export class AssistantApp extends Manager {
config: AssistantConfig; config: AssistantConfig;
pagesPath: string; pagesPath: string;
remoteIsConnected = false; remoteIsConnected = false;
attemptedConnectTimes = 0;
remoteApp: RemoteApp | null = null; remoteApp: RemoteApp | null = null;
remoteUrl: string | null = null; remoteUrl: string | null = null;
private resolver: ModuleResolver; private resolver: ModuleResolver;
@@ -87,7 +86,7 @@ export class AssistantApp extends Manager {
const enabled = opts?.enabled ?? share?.enabled ?? false; const enabled = opts?.enabled ?? share?.enabled ?? false;
if (share && enabled !== false) { if (share && enabled !== false) {
if (this.remoteApp) { if (this.remoteApp) {
this.remoteApp.ws?.close(); this.remoteApp.disconnect();
this.remoteApp = null; this.remoteApp = null;
this.remoteIsConnected = false; this.remoteIsConnected = false;
} }
@@ -100,23 +99,28 @@ export class AssistantApp extends Manager {
token, token,
id, id,
app: this.mainApp, app: this.mainApp,
// 使用 RemoteApp 内置的自动重连机制
autoReconnect: true,
reconnectDelay: 5000, // 首次重连延迟 5 秒
maxReconnectAttempts: Infinity, // 无限重连
enableBackoff: true, // 启用指数退避
}); });
const isConnect = await remoteApp.isConnect(); const isConnect = await remoteApp.isConnect();
if (isConnect) { if (isConnect) {
remoteApp.listenProxy(); remoteApp.listenProxy();
this.remoteIsConnected = true; this.remoteIsConnected = true;
// 清理已有的 close 事件监听器,防止多重绑定 // 监听连接成功和关闭事件
remoteApp.emitter.removeAllListeners('close'); remoteApp.on('open', () => {
remoteApp.emitter.on('close', () => { this.remoteIsConnected = true;
setTimeout(() => {
if (remoteApp.isError) {
console.error('远程应用发生错误,不重连');
} else {
this.reconnectRemoteApp();
}
}, 5 * 1000); // 第一次断开5秒后重连
});
logger.debug('链接到了远程应用服务器'); logger.debug('链接到了远程应用服务器');
});
remoteApp.on('close', () => {
this.remoteIsConnected = false;
console.log('远程连接已关闭,自动重连机制正在处理...');
});
remoteApp.on('maxReconnectAttemptsReached', () => {
logger.error('远程应用重连达到最大次数,停止重连');
});
const appId = id; const appId = id;
const username = config?.auth.username || 'unknown'; const username = config?.auth.username || 'unknown';
const url = new URL(`/${username}/v1/${appId}`, config?.registry || 'https://kevisual.cn/'); const url = new URL(`/${username}/v1/${appId}`, config?.registry || 'https://kevisual.cn/');
@@ -208,29 +212,6 @@ export class AssistantApp extends Manager {
} }
} }
} }
async reconnectRemoteApp() {
console.log('重新连接到远程应用服务器...', this.attemptedConnectTimes);
const remoteApp = this.remoteApp;;
if (remoteApp) {
// 先关闭旧的 WebSocket防止竞态条件
if (remoteApp.ws) {
remoteApp.ws.close();
}
remoteApp.init();
this.attemptedConnectTimes += 1;
const isConnect = await remoteApp.isConnect();
if (isConnect) {
remoteApp.listenProxy();
this.attemptedConnectTimes = 0;
console.log('重新连接到了远程应用服务器');
} else {
this.reconnectRemoteApp();
setTimeout(() => {
this.initRouterApp()
}, 30 * 1000 + this.attemptedConnectTimes * 10 * 1000); // 30秒后重连 + 每次增加10秒
}
}
}
async initRoutes() { async initRoutes() {
const routes = this.config.getConfig().routes || []; const routes = this.config.getConfig().routes || [];
for (const route of routes) { for (const route of routes) {

View File

@@ -0,0 +1,272 @@
# RemoteApp
RemoteApp 是一个 WebSocket 远程应用连接类,支持断开自动重连机制。
## 功能特性
- WebSocket 远程代理连接
- **断开自动重连**
- 指数退避算法
- 可配置重连策略参数
- 手动重连/断开控制
## 安装
```bash
npm install @kevisual/remote-app
```
## 基本使用
```typescript
import { RemoteApp } from '@kevisual/remote-app';
const app = new RemoteApp({
url: 'https://kevisual.cn/ws/proxy',
id: 'my-app',
token: 'your-token',
app: mainApp
});
// 等待连接建立
await app.isConnect();
// 监听代理消息
app.listenProxy();
```
## 断开重连配置
### 配置选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `app` | `App` | - | 本地应用实例 |
| `url` | `string` | - | 远程服务地址 |
| `id` | `string` | - | 应用 ID |
| `token` | `string` | - | 认证令牌 |
| `emitter` | `EventEmitter` | `new EventEmitter()` | 事件发射器 |
| `autoReconnect` | `boolean` | `true` | 是否启用自动重连 |
| `maxReconnectAttempts` | `number` | `Infinity` | 最大重连次数 |
| `reconnectDelay` | `number` | `1000` | 初始重连延迟(毫秒) |
| `maxReconnectDelay` | `number` | `30000` | 重连延迟最大值(毫秒) |
| `enableBackoff` | `boolean` | `true` | 是否启用指数退避 |
### 配置示例
```typescript
// 禁用自动重连
const app = new RemoteApp({
url: 'https://kevisual.cn/ws/proxy',
id: 'app1',
token: 'your-token',
autoReconnect: false
});
// 限制最大重连次数为 10 次
const app = new RemoteApp({
url: 'https://kevisual.cn/ws/proxy',
id: 'app1',
token: 'your-token',
maxReconnectAttempts: 10
});
// 自定义重连延迟策略(线性增长)
const app = new RemoteApp({
url: 'https://kevisual.cn/ws/proxy',
id: 'app1',
token: 'your-token',
enableBackoff: false, // 禁用指数退避
reconnectDelay: 2000, // 固定 2 秒延迟
maxReconnectAttempts5 // 最多重连 5 次
});
// 快速重连策略(适合内网环境)
const app = new RemoteApp({
url: 'ws://internal-server/ws',
id: 'app1',
reconnectDelay: 500, // 0.5 秒开始
maxReconnectDelay: 5000, // 最大 5 秒
enableBackoff: true
});
```
## 方法
### `isConnect(): Promise<boolean>`
检查或等待连接建立。
```typescript
const connected = await app.isConnect();
console.log(connected ? '已连接' : '连接失败');
```
### `disconnect()`
手动关闭连接并停止自动重连。
```typescript
app.disconnect();
```
### `reconnect()`
手动触发重连。
```typescript
app.reconnect();
```
### `json(data: any)`
发送 JSON 数据到远程服务。
```typescript
app.json({ type: 'ping' });
```
### `listenProxy()`
启动代理监听,处理远程请求。
```typescript
app.listenProxy();
````
##
###
| | | |
|------|------|------|
| `open` | `id: string` | |
| `close` | `id: string` | |
| `message` | `data: any` | |
| `error` | `error: any` | |
| `maxReconnectAttemptsReached` | `id: string` | |
| `reconnectFailed` | `{ id, attempt, error }` | |
###
```typescript
// 连接建立
app.on('open', (id) => {
console.log(` ${id} `);
});
// 连接关闭
app.on('close', (id) => {
console.log(` ${id} `);
});
// 收到消息
app.on('message', (data) => {
console.log('收到消息:', data);
});
// 发生错误
app.on('error', (error) => {
console.error('连接错误:', error);
});
// 达到最大重连次数
app.on('maxReconnectAttemptsReached', (id) => {
console.error(` ${id} `);
// 可以在这里提示用户或采取其他措施
});
// 重连失败
app.on('reconnectFailed', ({ id, attempt, error }) => {
console.error(` ${id} ${attempt} :`, error);
});
```
## 重连机制说明
### 重连流程
1. 连接断开时触发 `close` 事件
2. 检查是否满足自动重连条件(启用自动重连 + 非手动关闭)
3. 计算重连延迟(使用指数退避算法)
4. 达到最大重连次数则停止,触发 `maxReconnectAttemptsReached` 事件
5. 延迟后尝试重新连接
6. 连接成功则重置重连计数器,触发 `open` 事件
7. 连接失败则触发 `reconnectFailed` 事件并继续尝试重连
### 指数退避算法
当启用指数退避时,重连延迟按以下公式计算:
```
delay = initialDelay * 2^(attempts - 1)
```
实际延迟取上述值与 `maxReconnectDelay` 的较小值。
例如,初始延迟为 1000ms 时:
- 第 1 次重连1000ms (1s)
- 第 2 次重连2000ms (2s)
- 第 3 次重连4000ms (4s)
- 第 4 次重连8000ms (8s)
- 第 5 次重连16000ms (16s)
- 第 6 次重连30000ms (30s达到上限)
## 完整示例
```typescript
import { RemoteApp } from '@kevisual/remote-app';
class MyService {
private remoteApp: RemoteApp;
constructor() {
this.remoteApp = new RemoteApp({
url: 'https://kevisual.cn/ws/proxy',
id: 'my-service',
token: process.env.REMOTE_TOKEN,
maxReconnectAttempts: 10,
reconnectDelay: 1000,
maxReconnectDelay: 30000
});
this.setupEventListeners();
}
private setupEventListeners() {
this.remoteApp.on('open', () => {
console.log('远程连接已建立');
this.remoteApp.listenProxy();
});
this.remoteApp.on('close', () => {
console.log('远程连接已断开,正在尝试重连...');
});
this.remoteApp.on('maxReconnectAttemptsReached', () => {
console.error('重连失败,请检查网络连接或服务状态');
// 可以触发告警或通知管理员
});
}
async connect() {
const connected = await this.remoteApp.isConnect();
if (connected) {
console.log('连接成功');
} else {
console.error('连接超时');
}
}
disconnect() {
this.remoteApp.disconnect();
}
}
const service = new MyService();
service.connect();
```
## License
MIT

View File

@@ -0,0 +1,13 @@
{
"name": "@kevisual/remote-app",
"version": "0.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@kevisual/remote-app",
"version": "0.0.2",
"license": "MIT"
}
}
}

View File

@@ -1,20 +1,28 @@
{ {
"name": "@kevisual/remote-app", "name": "@kevisual/remote-app",
"version": "0.0.1", "version": "0.0.4",
"description": "", "description": "",
"main": "remote-app.ts", "main": "dist/app.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "build": "code-builder build -e remote-app.ts --dts"
}, },
"keywords": [], "keywords": [],
"files": [ "files": [
"dist",
"remote-app.ts" "remote-app.ts"
], ],
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"exports": {
".": "./dist/app.js"
},
"devDependencies": {
"eventemitter3": "^5.0",
"@kevisual/router": "^0.0.70"
},
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)", "author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.26.0", "packageManager": "pnpm@10.28.2",
"type": "module" "type": "module"
} }

View File

@@ -6,6 +6,16 @@ type RemoteAppOptions = {
token?: string; token?: string;
emitter?: EventEmitter; emitter?: EventEmitter;
id?: string; id?: string;
/** 是否启用自动重连,默认 true */
autoReconnect?: boolean;
/** 最大重连次数,默认 Infinity */
maxReconnectAttempts?: number;
/** 初始重连延迟(毫秒),默认 1000 */
reconnectDelay?: number;
/** 重连延迟最大值(毫秒),默认 30000 */
maxReconnectDelay?: number;
/** 是否启用指数退避,默认 true */
enableBackoff?: boolean;
}; };
/** /**
* 远程共享地址类似https://kevisual.cn/ws/proxy * 远程共享地址类似https://kevisual.cn/ws/proxy
@@ -19,6 +29,15 @@ export class RemoteApp {
ws: WebSocket; ws: WebSocket;
remoteIsConnected: boolean; remoteIsConnected: boolean;
isError: boolean = false; isError: boolean = false;
// 重连相关属性
autoReconnect: boolean;
maxReconnectAttempts: number;
reconnectDelay: number;
maxReconnectDelay: number;
enableBackoff: boolean;
reconnectAttempts: number = 0;
reconnectTimer: NodeJS.Timeout | null = null;
isManuallyClosed: boolean = false;
constructor(opts?: RemoteAppOptions) { constructor(opts?: RemoteAppOptions) {
this.mainApp = opts?.app; this.mainApp = opts?.app;
const token = opts.token; const token = opts.token;
@@ -32,6 +51,12 @@ export class RemoteApp {
_url.searchParams.set('id', id); _url.searchParams.set('id', id);
this.url = _url.toString(); this.url = _url.toString();
this.id = id; this.id = id;
// 初始化重连相关配置
this.autoReconnect = opts?.autoReconnect ?? true;
this.maxReconnectAttempts = opts?.maxReconnectAttempts ?? Infinity;
this.reconnectDelay = opts?.reconnectDelay ?? 1000;
this.maxReconnectDelay = opts?.maxReconnectDelay ?? 30000;
this.enableBackoff = opts?.enableBackoff ?? true;
this.init(); this.init();
} }
async isConnect(): Promise<boolean> { async isConnect(): Promise<boolean> {
@@ -39,6 +64,11 @@ export class RemoteApp {
if (this.isConnected) { if (this.isConnected) {
return true; return true;
} }
// 如果正在进行重连,等待连接成功
if (this.reconnectTimer !== null) {
console.log(`远程应用 ${this.id} 正在重连中...`);
}
// 等待连接成功(支持初次连接和重连场景)
return new Promise((resolve) => { return new Promise((resolve) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
resolve(false); resolve(false);
@@ -90,12 +120,84 @@ export class RemoteApp {
this.ws = ws; this.ws = ws;
} }
onOpen() { onOpen() {
this.isError = false;
this.reconnectAttempts = 0;
// 清除可能存在的重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.emitter.emit('open', this.id); this.emitter.emit('open', this.id);
} }
onClose() { onClose() {
console.log('远程应用关闭:', this.id); console.log('远程应用关闭:', this.id);
this.emitter.emit('close', this.id);
this.isConnected = false; this.isConnected = false;
this.emitter.emit('close', this.id);
// 触发自动重连逻辑
if (this.autoReconnect && !this.isManuallyClosed) {
this.scheduleReconnect();
}
}
/** 计算下一次重连延迟 */
calculateReconnectDelay(): number {
if (!this.enableBackoff) {
return this.reconnectDelay;
}
// 指数退避算法delay = initialDelay * 2^(attempts - 1)
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
return Math.min(delay, this.maxReconnectDelay);
}
/** 安排重连 */
scheduleReconnect() {
// 检查是否达到最大重连次数
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error(`远程应用 ${this.id} 已达到最大重连次数 ${this.maxReconnectAttempts},停止重连`);
this.emitter.emit('maxReconnectAttemptsReached', this.id);
return;
}
// 清除可能存在的定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
const delay = this.calculateReconnectDelay();
this.reconnectAttempts++;
console.log(`远程应用 ${this.id} 将在 ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
try {
this.init();
} catch (error) {
console.error(`远程应用 ${this.id} 重连失败:`, error);
this.emitter.emit('reconnectFailed', { id: this.id, attempt: this.reconnectAttempts, error });
// 重连失败后继续尝试重连
this.scheduleReconnect();
}
}, delay);
}
/** 手动关闭连接,停止自动重连 */
disconnect() {
this.isManuallyClosed = true;
this.autoReconnect = false;
// 清除重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// 关闭 WebSocket
if (this.ws) {
this.ws.close();
}
}
/** 手动重连 */
reconnect() {
this.isManuallyClosed = false;
this.reconnectAttempts = 0;
// 清除可能存在的定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.init();
} }
onMessage(data: any) { onMessage(data: any) {
this.emitter.emit('message', data); this.emitter.emit('message', data);
@@ -105,7 +207,7 @@ export class RemoteApp {
this.isError = true; this.isError = true;
this.emitter.emit('error', error); this.emitter.emit('error', error);
} }
on(event: 'open' | 'close' | 'message' | 'error', listener: (data: any) => void) { on(event: 'open' | 'close' | 'message' | 'error' | 'maxReconnectAttemptsReached' | 'reconnectFailed', listener: (data: any) => void) {
this.emitter.on(event, listener); this.emitter.on(event, listener);
return () => { return () => {
this.emitter.off(event, listener); this.emitter.off(event, listener);