magic-chat-im
v2.9.3
Published
基于 WebSocket 的即时通讯 SDK,提供完整的连接管理、消息收发、智能重连和心跳检测等功能。
Readme
IM SDK
基于 WebSocket 的即时通讯 SDK,提供完整的连接管理、消息收发、智能重连和心跳检测等功能。
特性
核心连接能力
✅ 智能重连机制 - 指数退避算法 + 随机抖动,避免惊群效应
✅ 连接状态管理 - 完整的 WebSocket 状态跟踪和管理
✅ 心跳保活检测 - 自动心跳机制,及时发现连接异常
✅ 网络状态监听 - 支持网络变化和 Tab 切换自动重连
✅ 移动端优化 - 针对移动设备的连接稳定性和性能优化
消息处理能力
✅ 可靠消息传输 - 断线期间消息不丢失,重连后自动发送
✅ 消息去重机制 - 基于 messageKey 的智能去重功能
✅ 消息队列管理 - 支持消息缓存和批量处理
✅ ACK确认机制 - 确保消息可靠到达
监控与诊断
✅ 连接统计信息 - 详细的连接、重连、消息统计
✅ 性能指标监控 - 连接成功率、消息丢失率等关键指标
✅ 日志系统 - 多级别日志记录和自定义日志处理器
✅ 错误追踪 - 完善的错误处理和异常上报机制
开发体验
✅ TypeScript支持 - 完整的类型定义,优秀的开发体验
✅ 事件驱动模型 - 灵活的事件系统,支持自定义监听
✅ 配置参数化 - 支持心跳、重连、日志等参数自定义
✅ API设计优雅 - 简洁直观的 API 设计,易于集成使用
安装
npm install @alife/magic-chat-im
# or
yarn add @alife/magic-chat-im
# or
pnpm add @alife/magic-chat-im快速开始
基础用法
``typescript import IM, { EVT_TYPE } from '@alife/magic-chat-im';
// 创建实例 const im = new IM({ url: 'wss://example.com/ws', appKey: 'your-app-key', getToken: async () => { // 动态获取 token return await fetchToken(); }, commonParams: { appId: 'your-app-id', userId: 'user-123', }, });
// 监听连接成功 im.on(EVT_TYPE.CONNECT, () => { console.log('连接成功'); });
// 监听消息 im.on(EVT_TYPE.MESSAGE, (msg) => { console.log('收到消息:', msg); });
// 发送消息 im.sendMessage({ msgType: 'text', content: 'Hello World', });
// 组件卸载时销毁 componentWillUnmount() { im.destroy(); }
### 带 TypeScript 类型
``typescript
import IM, {
EVT_TYPE,
type MessageBody,
type ReconnectEventData,
type ErrorEventData,
} from '@alife/magic-chat-im';
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
// 带类型的回调
messageCallback: (msg: MessageBody) => {
console.log('消息类型:', msg.msgType);
console.log('消息内容:', msg);
},
reconnectCallback: (data: ReconnectEventData) => {
console.log(`重连中... ${data.attempt}/${data.maxRetries}`);
console.log(`下次重连延迟: ${data.nextRetryDelay}ms`);
},
errorCallback: (err: ErrorEventData) => {
console.error('错误:', err.code, err.msg);
},
});在线demo
API 文档
构造函数
new IM(options: Options)
创建 IM 实例。
参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| url | string | 是 | WebSocket 服务器地址 |
| appKey | string | 是 | 应用标识 |
| token | string | 否 | 访问令牌(二选一) |
| getToken | () => Promise<string> | 否 | 动态获取 token 的函数(二选一) |
| commonParams | CommonParams | 否 | 全局参数(appId, userId) |
| reconnectMaxRetry | number | 否 | 10 | 最大重连次数(推荐 10-15) |
| supportNetworkRestoredReconnect | boolean | 否 | false | 是否启用网络状态监听 |
| loggerConfig | Partial<LoggerConfig> | 否 | 见说明 | 日志配置(默认根据环境自动配置) |
| connectCallback | () => void | 否 | 连接成功回调 |
| messageCallback | (msg: MessageBody) => void | 否 | 消息接收回调 |
| ackCallback | (msg: MessageData) => void | 否 | ACK 消息回调 |
| reconnectCallback | (data: ReconnectEventData) => void | 否 | 重连事件回调 |
| errorCallback | (err: ErrorEventData) => void | 否 | 错误回调 |
| closeCallback | (err: CloseEventData) => void | 否 | 连接关闭回调 |
| connectionTimeout | number | 否 | 15000 连接超时时间(毫秒),建议10-30秒 |
示例:
import { LogLevel } from '@alife/magic-chat-im';
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'app-123',
getToken: async () => {
const res = await fetch('/api/token');
return res.json().token;
},
commonParams: {
appId: 'app-123',
userId: 'user-456',
},
reconnectMaxRetry: 10, // 推荐 10-15 次 (约 3-5 分钟)
supportNetworkRestoredReconnect: true,
// 日志配置(可选)
loggerConfig: {
level: LogLevel.INFO, // 日志级别
enabled: true, // 是否启用
timestamp: true, // 显示时间戳
prefix: '[我的应用]', // 自定义前缀
handler: (level, message, ...args) => {
// 自定义处理器(如上报到服务器)
if (level === LogLevel.ERROR) {
reportToServer({ level, message, args });
}
},
},
messageCallback: (msg) => {
console.log('收到消息:', msg);
},
});实例方法
connect()
建立连接。
im.connect();sendMessage(msgBody: Partial<MessageBody>)
发送消息。
参数:
msgBody: 消息体对象
im.sendMessage({
msgType: 'text',
content: 'Hello',
});ping()
发送心跳。
im.ping();close()
主动关闭连接(不会触发重连)。
im.close();reconnect(code: RECONNECT_RESON, error?: string | Event)
手动触发重连。
im.reconnect(RECONNECT_RESON.POSTMESSAGE);manualReconnect()
手动重连(用于达到最大重连次数后)。
im.manualReconnect();destroy()
销毁实例,清理所有资源。
im.destroy();readyState(): number
获取 WebSocket 连接状态。
返回值:
0- CONNECTING:正在连接1- OPEN:已连接2- CLOSING:正在关闭3- CLOSED:已关闭
const state = im.readyState();
if (state === 1) {
console.log('已连接');
}setCommonParams(param: CommonParams)
设置全局参数。
im.setCommonParams({
appId: 'new-app-id',
userId: 'new-user-id',
});setUserClosed(closed: boolean)
设置用户关闭状态(用于测试)。
// 模拟系统错误,允许重连
im.setUserClosed(false);
// 模拟用户主动关闭,阻止重连
im.setUserClosed(true);getConnectionStats()
获取连接统计信息。
const stats = im.getConnectionStats();
console.log('连接统计:', stats);返回值:
{
totalConnections: number; // 总连接次数
totalReconnections: number; // 总重连次数
totalMessagesSent: number; // 总发送消息数
totalMessagesReceived: number; // 总接收消息数
totalHeartbeatTimeouts: number; // 总心跳超时次数
firstConnectionTime: number; // 首次连接时间戳
lastConnectTime: number; // 最后连接时间戳
lastDisconnectTime: number; // 最后断开时间戳
totalOnlineTime: number; // 总在线时长(毫秒)
currentOnlineTime: number; // 当前在线时长(毫秒)
}getPerformanceMetrics()
获取性能指标。
const metrics = im.getPerformanceMetrics();
console.log('性能指标:', metrics);返回值:
{
connectionSuccessRate: number; // 连接成功率
messageLossRate: number; // 消息丢失率
averageReconnectionTime: number; // 平均重连时间
heartbeatTimeoutRate: number; // 心跳超时率
}resetConnectionStats()
重置连接统计信息。
im.resetConnectionStats();监控方法
getUserClosed(): boolean
获取用户关闭状态。
const isUserClosed = im.getUserClosed();
console.log('是否用户主动关闭:', isUserClosed);getReconnectState(): ReconnectState | undefined
获取当前重连状态。
import { ReconnectState } from '@alife/magic-chat-im';
const state = im.getReconnectState();
if (state === ReconnectState.CONNECTING) {
console.log('正在重连中...');
}状态值:
ReconnectState.IDLE- 空闲,没有重连ReconnectState.SCHEDULED- 已调度,等待定时器触发ReconnectState.CONNECTING- 正在连接中
getReconnectCounter(): number
获取当前重连次数。
const count = im.getReconnectCounter();
const max = im.getReconnectMaxRetry();
console.log(`重连进度: ${count}/${max}`);getReconnectMaxRetry(): number
获取最大重连次数。
const maxRetry = im.getReconnectMaxRetry();
console.log('最大重连次数:', maxRetry);getQueueSize(): number
获取待发送消息队列大小。
const queueSize = im.getQueueSize();
if (queueSize > 50) {
console.warn('消息队列已积压:', queueSize);
}事件监听
on(event: EVT_TYPE, handler: Function)
监听事件。
事件类型:
| 事件 | 说明 | 回调参数 |
|------|------|----------|
| EVT_TYPE.OPEN | WebSocket 连接打开 | 无 |
| EVT_TYPE.CONNECT | 连接建立成功 | 无 |
| EVT_TYPE.MESSAGE | 收到业务消息 | MessageBody |
| EVT_TYPE.ACK | 收到 ACK 消息 | MessageData |
| EVT_TYPE.ERROR | 发生错误 | ErrorEventData |
| EVT_TYPE.CLOSE | 连接关闭 | CloseEventData |
| EVT_TYPE.RECONNECT | 重连事件 | ReconnectEventData |
示例:
// 监听连接
im.on(EVT_TYPE.CONNECT, () => {
console.log('已连接');
});
// 监听消息
im.on(EVT_TYPE.MESSAGE, (msg: MessageBody) => {
console.log('收到消息:', msg);
});
// 监听重连
im.on(EVT_TYPE.RECONNECT, (data: ReconnectEventData) => {
console.log(`重连中 ${data.attempt}/${data.maxRetries}`);
console.log(`下次延迟 ${data.nextRetryDelay}ms`);
});
// 监听错误
im.on(EVT_TYPE.ERROR, (err: ErrorEventData) => {
console.error('错误:', err.code, err.msg);
});
// 监听关闭
im.on(EVT_TYPE.CLOSE, (data: CloseEventData) => {
if (data.code === EXCEPTION_ERROR.NEED_RECONNECT_MANUAL) {
// 提示用户手动重连
showReconnectButton();
}
});类型定义
MessageBody
消息体类型。
interface MessageBody {
version?: string;
msgType?: string;
msgDetail?: any;
[key: string]: any;
}MessageData
完整消息数据。
interface MessageData {
name: string;
args?: any[];
messageKey: string;
msgType: RECEIVED_MSG_TYPE | SEND_MSG_TYPE;
msgBody: MessageBody;
appId?: string;
userId?: string;
}ReconnectEventData
重连事件数据。
interface ReconnectEventData {
code: RECONNECT_RESON;
err?: any;
attempt?: number;
maxRetries?: number;
nextRetryDelay?: number;
isReconnecting?: boolean;
}ErrorEventData
错误事件数据。
interface ErrorEventData {
code: EXCEPTION_ERROR;
err?: any;
msg?: string;
reason?: string;
maxRetry?: number;
[key: string]: any;
}CloseEventData
关闭事件数据。
interface CloseEventData {
code: EXCEPTION_ERROR;
err?: any;
reason?: string;
[key: string]: any;
}高级用法
处理最大重连次数
当达到最大重连次数时,会触发 NEED_RECONNECT_MANUAL 错误,此时需要用户手动重连。
``typescript import { EVT_TYPE, EXCEPTION_ERROR } from '@alife/magic-chat-im';
im.on(EVT_TYPE.CLOSE, (data) => { if (data.code === EXCEPTION_ERROR.NEED_RECONNECT_MANUAL) { // 显示重连按钮 showDialog({ title: '连接已断开', message: '请点击重连按钮', confirmText: '重连', onConfirm: () => { im.manualReconnect(); }, }); } });
### 监听网络状态变化
启用网络状态监听后,SDK 会自动处理网络变化 (浏览器`tab` 切换、网络断开、网络恢复):
``typescript
const im = new IM({
// ... 其他配置
supportNetworkRestoredReconnect: true,
});
// ⚠️ 重要:必须在销毁时调用 destroy, destroy 里面会调用close 关闭连接,清理定时器等
componentWillUnmount() {
im.destroy(); // 移除网络事件监听器
}消息队列
断线期间的消息会自动加入队列,重连后批量发送:
// 即使断线,消息也不会丢失
im.sendMessage({ content: 'Message 1' });
im.sendMessage({ content: 'Message 2' });
im.sendMessage({ content: 'Message 3' });
// 重连后,这些消息会按顺序发送自定义重连次数
const im = new IM({
// ... 其他配置
reconnectMaxRetry: 15, // 最多重连 15 次 (约 5.5 分钟)
// 不推荐超过 20 次,会导致过长的重连时间
});日志配置
基本使用(推荐)
import IM, { LogLevel } from '@alife/magic-chat-im';
// 方式 1:使用默认配置(根据环境自动调整)
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
// loggerConfig 可选,不传则使用默认配置
// 开发环境:{ level: DEBUG, enabled: true }
// 生产环境:{ level: WARN, enabled: false }
});
// 方式 2:自定义日志配置
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
loggerConfig: {
level: LogLevel.INFO, // 日志级别:DEBUG/INFO/WARN/ERROR/NONE
enabled: true, // 是否启用日志
timestamp: true, // 是否显示时间戳
prefix: '[我的应用]', // 自定义日志前缀
},
});集成监控系统(Sentry)
import IM, { LogLevel } from '@alife/magic-chat-im';
import * as Sentry from '@sentry/browser';
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
loggerConfig: {
level: LogLevel.DEBUG,
enabled: true,
handler: (level, message, ...args) => {
// 错误日志上报到 Sentry
if (level === LogLevel.ERROR) {
Sentry.captureMessage(message, {
level: 'error',
extra: { args },
});
}
},
},
});根据环境配置
import IM, { LogLevel } from '@alife/magic-chat-im';
const getLoggerConfig = () => {
switch (process.env.NODE_ENV) {
case 'development':
return { level: LogLevel.DEBUG, enabled: true };
case 'staging':
return { level: LogLevel.INFO, enabled: true };
case 'production':
return { level: LogLevel.WARN, enabled: false };
default:
return { level: LogLevel.ERROR, enabled: false };
}
};
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
loggerConfig: getLoggerConfig(),
});日志级别说明:
| 级别 | 说明 | 适用场景 |
|------|------|----------|
| LogLevel.DEBUG | 调试信息(最详细) | 开发环境 |
| LogLevel.INFO | 一般信息 | 测试环境 |
| LogLevel.WARN | 警告信息 | 生产环境 |
| LogLevel.ERROR | 错误信息 | 所有环境 |
| LogLevel.NONE | 不输出日志 | 特殊场景 |
日志输出示例:
[IM SDK] [14:23:45.123] [INFO] Network is back online. Attempting to reconnect...
[IM SDK] [14:23:45.456] [WARN] ActionQueue is full, dropping oldest action
[IM SDK] [14:23:45.678] [ERROR] Heartbeat timeout, initiating reconnect默认配置:
当不传 loggerConfig 时,使用以下默认配置:
// 开发环境 (process.env.NODE_ENV !== 'production')
{
level: LogLevel.DEBUG,
enabled: true,
prefix: '[IM SDK]',
timestamp: true,
}
// 生产环境 (process.env.NODE_ENV === 'production')
{
level: LogLevel.WARN,
enabled: false, // 禁用 INFO/DEBUG,ERROR 仍会输出
prefix: '[IM SDK]',
timestamp: true,
}React 集成示例
import { useEffect, useRef } from 'react';
import IM, { EVT_TYPE } from '@alife/magic-chat-im';
function Chat() {
const imRef = useRef<IM | null>(null);
useEffect(() => {
// 创建 IM 实例
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
supportNetworkRestoredReconnect: true,
messageCallback: (msg) => {
// 处理消息
handleMessage(msg);
},
});
imRef.current = im;
// 组件卸载时销毁
return () => {
im.destroy();
};
}, []);
const sendMessage = (content: string) => {
imRef.current?.sendMessage({
msgType: 'text',
content,
});
};
return (
<div>
<button onClick={() => sendMessage('Hello')}>
发送消息
</button>
</div>
);
}Vue 集成示例
import { onMounted, onUnmounted, ref } from 'vue';
import IM, { EVT_TYPE } from '@alife/magic-chat-im';
export default {
setup() {
const im = ref<IM | null>(null);
onMounted(() => {
im.value = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => 'your-token',
supportNetworkRestoredReconnect: true,
messageCallback: (msg) => {
// 处理消息
console.log('收到消息:', msg);
},
});
});
onUnmounted(() => {
im.value?.destroy();
});
const sendMessage = (content: string) => {
im.value?.sendMessage({
msgType: 'text',
content,
});
};
return {
sendMessage,
};
},
};常见问题
1. 如何判断连接是否成功?
im.on(EVT_TYPE.CONNECT, () => {
console.log('连接成功');
});2. 消息发送失败怎么办?
消息会自动加入队列,重连后自动重发,无需手动处理。
3. 如何处理重连失败?
im.on(EVT_TYPE.CLOSE, (data) => {
if (data.code === EXCEPTION_ERROR.NEED_RECONNECT_MANUAL) {
// 提示用户手动重连
alert('连接失败,请手动重连');
}
});4. 如何查看重连进度?
im.on(EVT_TYPE.RECONNECT, (data) => {
console.log(`重连进度: ${data.attempt}/${data.maxRetries}`);
console.log(`下次重连延迟: ${data.nextRetryDelay}ms`);
});5. 是否支持多实例?
支持,每个实例独立管理连接。
const im1 = new IM({ ... });
const im2 = new IM({ ... });6. 如何处理 Token 过期?
使用 getToken 函数动态获取 Token:
const im = new IM({
url: 'wss://example.com/ws',
appKey: 'your-app-key',
getToken: async () => {
// 每次连接时都会调用此函数获取最新 Token
const res = await fetch('/api/refresh-token');
return res.json().token;
},
});注意事项
⚠️ 重要提醒:
网络状态监听
如果启用了supportNetworkRestoredReconnect,必须在组件卸载时调用destroy()方法,否则会有内存泄漏。手动重连
manualReconnect()会清空当前队列并重新建立连接,请谨慎使用。消息去重
消息去重队列最多保存 100 条消息 key,超过会自动移除最老的。队列大小
ActionQueue 最多保存 100 条待发送消息,超过会移除最老的。重连策略
重连采用指数退避算法,延迟会逐渐增加:1s → 2s → 4s → 8s → 16s → 30s(最大)事件监听器清理
在调用destroy()方法之前,请确保移除所有手动添加的事件监听器,以避免内存泄漏。虽然SDK会自动清理内部事件监听器,但无法自动清理用户代码中通过on()方法添加的监听器。
Changelog
详细的变更记录请参阅:CHANGELOG.md
License
MIT
贡献
欢迎提交 Issue 和 Pull Request!
支持
如有问题,请提交 Issue 或联系维护团队。
