初始化、登入與連線
本頁講客戶端從建立到釋放的全過程:建立、登入、保持登入、顯示連線狀態、退出。
典型的用法是:頁面載入時建立客戶端;im.auth.currentUser 為 null 時顯示登入頁,用業務服務端簽發的憑證登入;監聽 auth.stateChanged,會話結束時回到登入頁;監聽 connection.stateChanged,在介面上顯示“連線中”“網路不可用”等狀態。
| 要做什麼 | 看哪裡 |
|---|---|
| 建立客戶端、配置選項 | 建立客戶端、初始化選項 |
| 用業務服務端簽發的憑證登入,憑證失效時自動重新獲取 | 用登入憑證登入、自動重新獲取憑證 |
| 使用者名稱密碼登入、註冊 | 用密碼登入、註冊 |
| 重新整理頁面後保持登入 | 恢復登入 |
| 被擠下線、被封禁時回到登入頁 | 會話結束 |
| 顯示“連線中”“網路不可用” | 連線狀態 |
| 按應用配置顯示或隱藏功能 | 執行配置 |
| 處理多個標籤頁、避免重複提醒 | 同一瀏覽器的多個標籤頁 |
| 排查問題 | 診斷、日誌 |
建立客戶端
import { createDRClient } from '@deeprespond/im-web';
const im = createDRClient({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
});- 一個頁面一個客戶端:同一頁面中每個 AppKey 同一時刻只能有一個客戶端,已有時再建立丟擲
invalid_state(details.reason為client_exists)。請在一個模組中建立,整個頁面共用,不要在元件中反覆建立。同一頁面要同時使用兩個應用時,可以各建一個客戶端,它們互不相干。 - 建立時不發請求:建立後立即讀取本地儲存的登入狀態,
im.auth.state、im.auth.currentUser馬上可用。這個瀏覽器中已經登入過、會話還沒有結束的,SDK 自動恢復登入,見恢復登入。 - 只能在瀏覽器中建立:在服務端渲染(Node.js)中呼叫,或瀏覽器不滿足執行環境的要求時,丟擲
unsupported。 appKey、apiUrl格式不對,或appVersion超長時,丟擲local_validation,details.field指出是哪個選項。
釋放客戶端
im.destroy() 關閉長連線、釋放全部列表,之後客戶端不再發出事件,非同步方法以 invalid_state(details.reason 為 destroyed)拒絕。它不會退出登入:之後再建立的客戶端會恢復同一個登入。
destroy() 之後可以立即建立新的客戶端,不必等它完成:新客戶端會等舊的關閉連線和本地儲存之後再開始工作,這期間的呼叫排隊等待。所以開發時的熱更新可以這樣寫:
import { createDRClient } from '@deeprespond/im-web';
export const im = createDRClient({ appKey: '1575529652#demo', apiUrl: 'https://im.example.com' });
// Vite 的热更新会重新执行这个模块:先释放旧的客户端,新的才能创建
if (import.meta.hot) import.meta.hot.dispose(() => void im.destroy());關閉頁面前,可以用 im.hasPendingWork() 判斷是否還有沒發出的訊息或沒完成的上傳,提醒使用者:
window.addEventListener('beforeunload', (e) => {
if (im.hasPendingWork()) e.preventDefault();
});沒發出的訊息儲存在本地,下次開啟頁面後會繼續傳送(超過 48 小時仍沒發出的不再傳送);超過 50 MB 的附件不隨傳送佇列儲存,關閉頁面後這條訊息會傳送失敗。
初始化選項
| 選項 | 型別 | 必填 | 說明 |
|---|---|---|---|
appKey | string | 是 | 應用的 AppKey,格式為 org_name#app_name,在控制檯的應用詳情中檢視 |
apiUrl | string | 是 | IM 服務的地址,如 https://im.example.com,必須以 http:// 或 https:// 開頭 |
wsUrl | string | 否 | 長連線的地址。預設把 apiUrl 的協議換成 wss://(http:// 換成 ws://)並加上 /client/v1/ws |
platform | 'web' | 'windows' | 'macos' | 'linux' | 否 | 登入時上報的平臺,預設 web。Electron 應用傳所在的系統,這樣按平臺限制裝置數時算作電腦 |
deviceName | string | 否 | 裝置名稱,顯示在登入裝置列表和“你的賬號已在 XX 上登入”的提示中。預設由瀏覽器和系統拼成,如 Chrome · Windows、Safari · iPhone。最長 64 個字元,超長截斷 |
appVersion | string | 否 | 你的應用的版本號,隨長連線上報,便於排查問題。按 UTF-8 編碼最長 64 位元組 |
ticketProvider | (ctx: TicketContext) => Promise<string> | 否 | 重新獲取登入憑證的函式,建議使用憑證登入的應用都提供,見自動重新獲取憑證 |
autoMarkRead | boolean | 否 | 使用者正在看一個會話時,自動把新訊息標記為已讀。預設 false,見自動標記已讀 |
conversationWindow | number | 否 | 會話列表在記憶體中最多保留的會話數,預設 1000,見會話列表的視窗 |
userCacheTtlMs | number | 否 | 其他使用者資料的快取時間,單位為毫秒,預設 600000(10 分鐘) |
storage | 'indexeddb' | 'memory' | 否 | 本地資料儲存在哪裡,預設 indexeddb。memory 時只儲存在記憶體中,重新整理頁面後從服務端重新載入 |
persistStorage | boolean | 否 | 登入後向瀏覽器申請持久儲存,降低瀏覽器在磁碟空間不足時清除本地資料的可能。預設 false |
tokenStorage | 'local' | 'memory' | 否 | 登入令牌儲存在哪裡,預設 local(localStorage,所有標籤頁共用)。memory 時只儲存在記憶體中,重新整理頁面後要重新登入,並且按 sessionScope: 'tab' 處理,見令牌的儲存 |
sessionScope | 'browser' | 'tab' | 否 | 預設 browser:同一瀏覽器的所有標籤頁共用一次登入。tab 只用於演示和測試,見每個標籤頁單獨登入 |
logger | { level?: LogLevel; sink?: (entry: LogEntry) => void } | 否 | 日誌級別和輸出函式,見日誌 |
自動標記已讀
autoMarkRead 為 true 時,使用者“正在看”一個會話期間收到的新訊息自動標記為已讀。“正在看”指:這個會話有開啟著的訊息列表,列表透過 setViewing() 報告停在最新處(atBottom)且在介面上顯示(visible),並且頁面可見(沒有報告過的按 true 處理)。預設 false,這時由你在合適的時機呼叫 list.markRead() 或 im.conversations.markRead(),見會話。開啟後,介面上被隱藏但沒有釋放的列表要報告 visible: false,否則使用者沒看到的訊息也會被標為已讀。
會話列表的視窗
conversationWindow 是會話列表在記憶體中最多保留的會話數,預設 1000。im.conversations.list() 第一次從本地讀取最近的這麼多個會話,之後呼叫 loadMore() 每次再讀 100 個,本地讀完後再向服務端取更早的會話。會話很多、記憶體緊張時可以調小。未讀總數 getUnreadTotal() 總是覆蓋全部會話,不受這個視窗限制。
裝置標識與裝置名稱
SDK 為每個瀏覽器生成一個裝置標識(web_ 加 32 位十六進位制隨機數),儲存在 localStorage 中,同一瀏覽器(同一網站)的所有標籤頁共用,你不需要處理。服務端按裝置標識區分登入裝置:同一個瀏覽器再次登入會替換這個瀏覽器原來的登入,不會多佔裝置數。
使用者清除了網站資料時,裝置標識隨之重新生成,服務端把這個瀏覽器當作新裝置,舊的登入一直佔用裝置名額,直到過期或被擠下線。Safari 會清除 7 天沒有訪問過的網站的全部本地資料,結果相同:使用者要重新登入。這是瀏覽器的限制。
登入
客戶端支援兩種登入方式,與服務端文件中的客戶端登入相同:
| 方式 | 方法 | 適用場景 |
|---|---|---|
| 登入憑證(推薦) | im.auth.loginWithTicket() | 已有賬號體系的應用:使用者登入你的業務系統後,業務服務端簽發登入憑證,網頁用憑證登入 IM |
| 密碼 | im.auth.loginWithPassword() | 由 IM 服務儲存使用者密碼的應用 |
登入時 SDK 自動帶上 AppKey、裝置標識、裝置名稱、平臺和 SDK 版本,你只需要提供憑證或使用者名稱和密碼。
用登入憑證登入
const { user, config, session_id } = await im.auth.loginWithTicket({ ticket });
console.log(user.nickname, config.max_message_body_bytes, session_id);憑證 5 分鐘內有效、只能使用一次:提交登入後即作廢,即使登入失敗也要重新獲取。可以同時傳 username,SDK 會校驗憑證屬於這個使用者。
自動重新獲取憑證
建立客戶端時提供 ticketProvider,SDK 在兩種情況下呼叫它重新獲取憑證:
- 用憑證登入返回
invalid_credentials(憑證無效、已使用或已過期)時,重新獲取一次再登入,ctx.reason為invalid_credentials;仍然失敗的才把錯誤返回給你; - 登入狀態過期(使用者很久沒有開啟網頁,超過了續期的有效期)時,靜默重新登入一次,
ctx.reason為session_expired,ctx.username為原來的使用者。成功時使用者不會感覺到,失敗時會話結束。
import { createDRClient } from '@deeprespond/im-web';
const im = createDRClient({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
ticketProvider: async ({ reason, username }) => {
const res = await fetch('/api/im-ticket', { method: 'POST', credentials: 'include' });
if (!res.ok) throw new Error('业务系统未登录'); // 抛出异常表示取不到,SDK 按登录失败处理
return ((await res.json()) as { ticket: string }).ticket;
},
});ticketProvider 應只為當前登入你的業務系統的使用者簽發憑證。被踢下線、被封禁、修改了密碼等原因結束的會話不會自動重新登入。
用密碼登入
await im.auth.loginWithPassword({ username: 'zhangsan', password: 'Passw0rd!' });使用者名稱不區分大小寫。使用者需要先設定密碼,見建立使用者。
註冊
應用的註冊模式為 open 時,使用者可以在網頁中自行註冊,見客戶端自注冊。註冊不會自動登入:
await im.auth.register({ username: 'xiaoming', password: 'secret123', nickname: '小明' });
await im.auth.loginWithPassword({ username: 'xiaoming', password: 'secret123' });新建的應用預設不允許客戶端註冊(返回 permission_denied),只建議在測試應用中開啟。
切換使用者
已有另一個使用者登入時,直接登入新使用者即可:SDK 先讓原來的使用者退出(本地資料保留),再登入新使用者,同一瀏覽器的其他標籤頁一起切換到新使用者,併發出 auth.userSwitched 事件。
登入失敗
登入失敗時以 DRError 拒絕,登入狀態不變:
code | 原因 | 建議 |
|---|---|---|
invalid_credentials | 使用者名稱或密碼錯誤;憑證無效、已使用或已過期 | 密碼登入提示“使用者名稱或密碼錯誤”;憑證登入時已提供 ticketProvider 的,SDK 已經重新獲取過一次 |
user_disabled | 使用者已被封禁 | 提示“賬號已被封禁” |
too_many_attempts | 密碼錯誤次數過多,暫時不能用密碼登入 | 按 err.retryAfter(秒)提示使用者稍後再試,不要自動重試 |
device_limit_exceeded | 登入裝置數已滿,且應用設定為拒絕新裝置登入 | 提示使用者在其他裝置上退出;憑證已作廢,重試要重新獲取 |
tenant_unavailable、app_unavailable | 服務已暫停 | 提示“服務暫時不可用” |
not_found | AppKey 對應的應用不存在 | 檢查 appKey 的配置 |
rate_limited | 登入過於頻繁 | SDK 已在等待時間不超過 60 秒時自動重試一次;仍失敗的按 err.retryAfter 稍後再試 |
network_error、timeout、internal | 網路或服務端故障 | 稍後重試;憑證登入要重新獲取憑證 |
local_validation | 憑證、使用者名稱或密碼為空 | 請求沒有發出,檢查引數 |
import { DRError } from '@deeprespond/im-web';
import { describeError } from '@deeprespond/im-web/locale/zh-CN';
try {
await im.auth.loginWithPassword({ username: 'zhangsan', password: 'wrong' });
} catch (err) {
if (err instanceof DRError && err.code === 'invalid_credentials') showToast('用户名或密码错误');
else if (err instanceof DRError) showToast(describeError(err));
else throw err;
}恢復登入
頁面開啟時,這個瀏覽器中已經登入過、會話還沒有結束的,建立客戶端後立即處於已登入狀態(im.auth.state.kind 為 signed_in):
- 本地儲存的會話列表、訊息等資料立即可以讀取,可以馬上顯示介面,不必等待網路;
- SDK 在後臺連線長連線、同步離線期間的變化,完成後連線狀態變為
ready,併發出sync.completed事件; - 令牌已過期的,SDK 先續期再連線。
所以頁面啟動時按 im.auth.currentUser 是否為 null 決定顯示登入頁還是主介面:
if (im.auth.currentUser) {
render('main', im.auth.currentUser.username);
} else {
render('login');
}退出
await im.auth.logout();
// 多人共用的电脑上:退出并删除这个用户在本地的聊天记录
await im.auth.logout({ clearLocalData: true });- 退出在本地立即生效:清除令牌、斷開長連線,開啟著的列表被清空並釋放,
auth.state變為signed_out。同一瀏覽器的其他標籤頁一起退出。 - SDK 同時通知服務端結束這次登入,釋放裝置名額。網路不通時 SDK 記下來,之後網路恢復或下次開啟頁面時補發,
logout()不會因此失敗。 - 預設保留本地資料:同一使用者再次登入時可以立即顯示之前的會話和訊息。
要刪除不是當前登入使用者的本地資料(如“清除本機上的聊天記錄”),使用 clearLocalData:
await im.auth.clearLocalData({ username: 'lisi' }); // 删除 lisi 在本地的数据
await im.auth.clearLocalData({ all: true }); // 删除本应用的全部本地数据,只能在未登录时调用不能用它刪除當前登入使用者的資料,請改用 logout({ clearLocalData: true })。
修改密碼
await im.auth.changePassword({ current_password: 'Passw0rd!', new_password: 'N3wPassw0rd' });成功後這臺裝置保持登入(SDK 換用新的令牌,並重新連線),這個使用者的其他裝置全部下線,下線原因為 password_changed。
- 當前密碼錯誤以
invalid_credentials拒絕,不會結束會話,但計入密碼錯誤次數;錯誤次數過多返回too_many_attempts; - 沒有設定過密碼的使用者(
im.users.me()?.has_password為false,如透過憑證登入自動建立的使用者)呼叫返回permission_denied; - 修改密碼不會自動重試:網路錯誤時,請讓使用者確認後再操作。
會話結束
以下情況下這次登入被服務端結束,SDK 清除本地令牌、斷開連線,所有標籤頁的 auth.state 變為 { kind: 'ended', code, reason, details },併發出 auth.stateChanged。開啟著的列表被清空並釋放,你應回到登入頁:
im.on('auth.stateChanged', (state) => {
if (state.kind !== 'ended') return;
if (state.reason === 'device_limit' || state.reason === 'replaced') {
const device = typeof state.details?.device_name === 'string' ? state.details.device_name : '';
showToast(device ? `你的账号已在 ${device} 上登录` : '你的账号已在其他设备上登录');
} else if (state.reason === 'user_disabled') {
showToast('账号已被封禁');
} else {
showToast('登录已失效,请重新登录');
}
render('login');
});code | reason | 原因 | 本地資料 |
|---|---|---|---|
session_revoked | device_limit | 登入裝置數超出上限,被新裝置擠下線。details.device_name、details.platform 為新裝置(可能為空) | 保留 |
session_revoked | replaced | 同一個裝置標識在別處重新登入(通常是複製了瀏覽器的本地資料)。details 同上 | 保留 |
session_revoked | kicked | 被業務服務端、控制檯踢下線,或被使用者本人在其他裝置上踢下線 | 保留 |
session_revoked | password_changed | 密碼在其他裝置上被修改,或被服務端重置 | 保留 |
session_revoked | user_disabled | 使用者被封禁 | 保留 |
session_revoked | relogin_required | 控制檯要求應用內全部使用者重新登入 | 保留 |
session_revoked | token_reuse | 續期令牌被重複使用,可能已經洩露 | 保留 |
session_revoked | expired | 登入過期(超過有效期沒有開啟網頁)。提供了 ticketProvider 的,SDK 先靜默重新登入一次,失敗才結束 | 保留 |
session_revoked | user_deleted | 使用者被刪除 | 刪除這個使用者的本地資料 |
unauthenticated | app_deleted、tenant_closed | 應用已刪除,或租戶已登出 | 刪除本應用的全部本地資料 |
unauthenticated | 無 | 續期令牌已失效。提供了 ticketProvider 的,SDK 先靜默重新登入一次 | 保留 |
下線原因的完整說明見服務端文件的下線原因。本人在其他標籤頁中退出時,這個標籤頁的狀態變為 signed_out,不是 ended,不需要提示。
服務暫停
租戶或應用被暫停服務(如欠費)時,auth.state 變為 { kind: 'suspended', code, reason, retryAt }:code 為 tenant_unavailable 或 app_unavailable,retryAt 為 SDK 下一次嘗試恢復的時刻(毫秒時間戳)。這期間登入保持,本地資料照常可讀,長連線和同步暫停,發出的訊息在傳送佇列中等待。SDK 每隔幾分鐘檢查一次,服務恢復後自動回到 signed_in,繼續連線和傳送,不需要使用者重新登入。reason 為 arrears 時可以提示“服務已暫停”。
應用處於只讀狀態時,寫操作返回 app_unavailable,但長連線和查詢照常,auth.state 不變。
令牌續期
登入後得到的令牌有有效期(由執行策略決定,預設 1 天),SDK 在過期之前自動續期,同一瀏覽器的多個標籤頁只發出一次續期請求,你不需要處理。
- 續期因網路原因失敗時,SDK 在 5 分鐘內多次重試;仍失敗的等下一次需要令牌時再試,已有的連線不受影響。
- 使用者在續期令牌的有效期(預設 7 天)內開啟過網頁,就一直保持登入;超過的,登入過期,見上文的
expired。 - 已知的限制:續期請求已被服務端處理、而瀏覽器沒有收到響應,隨後又關閉了瀏覽器超過 10 分鐘的,下次開啟時會話會以
token_reuse結束,使用者要重新登入。
令牌的儲存
令牌預設儲存在 localStorage 中,這樣所有標籤頁共用一次登入、重新整理頁面後保持登入。同一網站的任何指令碼都能讀到 localStorage,所以請設定嚴格的內容安全策略(CSP),不要在頁面中插入不可信的 HTML。
對安全要求高的應用,可以設定 tokenStorage: 'memory',令牌只儲存在記憶體中:重新整理頁面後要重新登入,每個標籤頁要分別登入(按每個標籤頁單獨登入處理),每個標籤頁各佔一個裝置名額。
SDK 的請求不帶 Cookie,令牌只發給 IM 服務,不會發給檔案儲存、CDN 或媒體服務。
連線狀態
im.connection.state 是長連線的當前狀態,變化時發出 connection.stateChanged:
kind | 含義 | 介面建議 |
|---|---|---|
idle | 未登入,或還沒開始連線 | |
connecting | 正在連線 | “連線中…” |
authenticating | 已連上,正在認證 | “連線中…” |
syncing | 已認證,正在同步離線期間的變化,pending 為還沒完成的模組 | “收取中…” |
ready | 已就緒,實時收發訊息 | 不顯示 |
waiting | 連線斷開,等待重連。retryAt 為下一次重連的時刻(毫秒時間戳),lastError 為最近一次斷開的原因 | “連線已斷開,正在重連” |
offline | 瀏覽器報告網路不可用 | “網路不可用” |
stopped | 已停止,不會自動重連,code 說明原因,見下表 | 按原因提示 |
連線斷開後 SDK 按 1 秒起逐次翻倍、最長 60 秒的間隔自動重連;網路恢復、頁面回到前臺時立即重連。斷開期間本地資料照常可讀,傳送的訊息進入傳送佇列,連上後自動發出。
stopped 的原因:
code | 原因 | 處理 |
|---|---|---|
upgrade_required | SDK 版本低於服務端要求的最低版本,details.min_version 為最低版本。同時停止傳送和同步,本地資料只讀,寫操作以 invalid_state(details.reason 為 upgrade_required)拒絕;另發出 sdk.upgradeRequired 事件 | 提示使用者重新整理頁面或升級應用 |
replaced_elsewhere | 同一次登入在別處建立了連線(可能是令牌洩露),details.ip、details.connected_at 為新連線的資訊 | 由使用者決定是否退出後重新登入 |
refresh_required | 同一瀏覽器中開啟了新版本的頁面,或本地資料被新版本升級,這個頁面只能讀取本地資料;另發出 tabs.refreshRequired 事件 | 提示使用者重新整理頁面 |
invalid_auth | 認證的內容不合法,details.field 指出欄位,通常是 appVersion 等配置有誤 | 檢查配置 |
im.on('connection.stateChanged', (state) => {
switch (state.kind) {
case 'ready':
render('');
break;
case 'offline':
render('网络不可用');
break;
case 'waiting':
render(`连接已断开,${Math.ceil((state.retryAt - Date.now()) / 1000)} 秒后重连`);
break;
case 'stopped':
render(state.code === 'upgrade_required' || state.code === 'refresh_required' ? '请刷新页面' : '连接已停止');
break;
default:
render('连接中…');
}
});需要在連線就緒後才做的事,可以用 waitUntilReady 等待:
await im.connection.waitUntilReady({ timeoutMs: 10000 }); // 超时以 timeout 拒绝一般不需要等待:訊息、會話等操作在未連線時也可以呼叫,SDK 會排隊或改用 HTTP 請求。
執行配置
im.config 是應用的執行配置,由控制檯的執行策略決定,登入時取得並儲存在本地,之後在同步中自動更新,變化時發出 config.changed。未登入時為 null。
用它決定介面上顯示哪些功能,例如沒有開啟音影片的應用不顯示通話按鈕:
const config = im.config;
const showCallButton = config?.rtc_enabled === true;
const showReactions = config?.message_reaction_enabled === true;
const maxBytes = config?.max_message_body_bytes ?? 5120;
im.on('config.changed', (next) => {
render({ calls: next?.rtc_enabled === true });
});常用的欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
max_online_devices_per_user | number | 每個使用者同時登入的裝置數上限 |
max_message_body_bytes | number | 一條訊息的大小上限,單位為位元組 |
recall_window_seconds | number | 撤回自己訊息的時限,單位為秒,0 為不能撤回 |
single_read_ack_enabled、group_read_ack_enabled | boolean | 是否開啟單聊、群聊的已讀回執 |
message_reaction_enabled | boolean | 是否允許表情回應 |
max_pinned_messages_per_conversation | number | 每個會話最多置頂的訊息數 |
message_retention_days | number | 歷史訊息的保留天數 |
friend_check_enabled | boolean | 是否只能給好友發單聊訊息 |
user_blacklist_enabled | boolean | 是否允許拉黑。為 false 時不提供拉黑的入口 |
client_group_create_enabled | boolean | 是否允許在客戶端建群 |
max_group_members | number | 群人數上限 |
max_upload_bytes | number | 訊息附件和群檔案的大小上限,單位為位元組 |
group_file_enabled | boolean | 是否允許在客戶端上傳群檔案 |
presence_enabled | boolean | 是否允許查詢和訂閱線上狀態 |
client_chatroom_create_enabled | boolean | 是否允許在客戶端建立聊天室 |
rtc_enabled | boolean | 是否開啟音視訊通話 |
report_enabled | boolean | 是否允許舉報 |
各欄位的含義和取值範圍見執行策略。以後新增的欄位同樣會出現在 im.config 中。
同一瀏覽器的多個標籤頁
同一瀏覽器中同一個應用的所有標籤頁共用一個裝置標識、一次登入和一條長連線。你在每個標籤頁中照常建立客戶端、呼叫方法即可,以下行為由 SDK 處理:
- 主標籤頁:其中一個標籤頁是主標籤頁,負責長連線、同步、寫入本地資料和傳送訊息。只開一個標籤頁時它就是主標籤頁。主標籤頁關閉、重新整理或卡住時,另一個標籤頁在幾秒到二十幾秒內自動接替。
im.connection.isLeader表示本標籤頁是否為主標籤頁。 - 呼叫的轉發:在其他標籤頁中呼叫需要請求服務端的方法(如發訊息、修改資料),SDK 轉給主標籤頁執行,結果原樣返回。主標籤頁正在切換時呼叫會等待,最長 10 秒,超時以
leader_unavailable拒絕;發訊息、標記已讀這類可以安全重發的呼叫會自動交給新的主標籤頁,建群、傳送好友申請這類呼叫請讓使用者確認後再試。 - 資料與事件:所有標籤頁讀取同一份本地資料,每個標籤頁都會收到同樣的事件,開啟著的列表自動更新;各標籤頁的連線狀態與主標籤頁相同。
- 登入與退出:一個標籤頁登入、退出或切換使用者,其他標籤頁隨之改變。請不要讓各標籤頁分別登入:同一個裝置標識的登入會互相替換。
- 提醒:新訊息、來電這類提醒,每個標籤頁都會收到事件。事件載荷中的
alert為true時,本標籤頁是負責提醒的標籤頁(有可見的標籤頁時為最近獲得焦點的那個,否則為主標籤頁),請只在這時彈出通知、播放鈴聲,避免重複提醒。im.connection.isAlertTab表示本標籤頁當前是否負責提醒。 - 新舊版本:你釋出新版本後,同一瀏覽器中可能同時開著新舊兩個版本的頁面。新版本的頁面接替主標籤頁,舊版本的頁面連線狀態變為
stopped(refresh_required)併發出tabs.refreshRequired,請提示使用者重新整理。
本標籤頁成為或不再是主標籤頁、提醒標籤頁時,發出 tabs.roleChanged 事件。
降級模式
以下情況下標籤頁之間的協調可靠性較低,SDK 照常工作,但在控制檯列印警告:
- HTTP 頁面:不是安全上下文,瀏覽器不提供 Web Locks,SDK 改用
localStorage協調主標籤頁,偶爾會短暫出現兩個標籤頁同時連線(隨後自動消解)。im.connection.getDiagnostics().degraded為true。正式環境請使用 HTTPS。 - 嵌入到其他網站的 iframe 中(如客服外掛):瀏覽器按頂級網站隔離 iframe 的儲存,嵌入在不同網站上的同一個應用各自是一臺裝置、各佔一個裝置名額;瀏覽器完全不給 iframe 儲存時(Safari 的部分設定),令牌不能儲存,重新整理頁面後要重新登入。這類場景請使用憑證登入,每次開啟頁面時由業務服務端簽發憑證。
每個標籤頁單獨登入
建立客戶端時設定 sessionScope: 'tab',每個標籤頁使用自己的裝置標識、各自登入、各自持有長連線,資料只儲存在記憶體中。這樣可以在同一個瀏覽器中同時登入兩個使用者互相聊天,只用於演示和自動化測試:每個標籤頁佔一個裝置名額,超出上限時可能把使用者的手機擠下線,不能用於正式的應用。
診斷
im.connection.getDiagnostics() 返回排查問題所需的資訊,使用者反饋問題時可以附上:
const d = im.connection.getDiagnostics();
console.log(d.sdkVersion, d.sessionId, d.connectionId, d.connectionState.kind);
console.log(d.recentDisconnects, d.recentFailures); // 最近 20 次断开、最近 20 个失败的请求診斷資訊不含令牌和訊息內容。其中的 sessionId 與服務端查詢登入裝置中的 session_id 相同,recentFailures 中的 requestId 可以提供給技術支援查詢請求。
日誌
SDK 預設只把 warn 和 error 級別的日誌輸出到瀏覽器控制檯。可以調整級別,或交給你自己的日誌系統:
import { createDRClient, type LogEntry } from '@deeprespond/im-web';
const logs: LogEntry[] = [];
const im = createDRClient({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
logger: {
level: 'info',
sink: (entry) => {
logs.push(entry);
if (logs.length > 500) logs.shift();
},
},
});日誌分為 error、warn、info、debug 四級。日誌中不包含令牌、密碼、登入憑證、訊息內容和檔案地址的金鑰部分,只記錄請求名、錯誤碼、request_id 和連線 ID,可以放心上報。sink 丟擲的異常被忽略,不影響 SDK。
介面參考
createDRClient()
建立客戶端。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
options | DRClientOptions | 是 | 初始化選項,見初始化選項 |
返回值:DRClient。
可能的錯誤(同步丟擲):invalid_state(client_exists:這個頁面已有同一個 AppKey 的客戶端)、unsupported(不在瀏覽器中,或瀏覽器缺少必需的能力,details.missing 列出缺少的能力)、local_validation(選項格式不對,details.field 指出選項)。
im.auth.loginWithTicket()
用業務服務端簽發的登入憑證登入。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.ticket | string | 是 | 登入憑證,以 ult_ 開頭 |
p.username | string | 否 | 憑證所屬的使用者名稱,給出時由服務端校驗 |
返回值:Promise<LoginResult>,兌現時本地儲存已經開啟,可以立即建立列表。
可能的錯誤:見登入失敗。
im.auth.loginWithPassword()
用使用者名稱和密碼登入。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.username | string | 是 | 使用者名稱,不區分大小寫 |
p.password | string | 是 | 密碼 |
返回值:Promise<LoginResult>。
可能的錯誤:見登入失敗。
im.auth.register()
在客戶端註冊新使用者。只在應用的註冊模式為 open 時可用,註冊後不會自動登入。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.username | string | 是 | 使用者名稱,規則見使用者名稱與資料 |
p.password | string | 是 | 密碼,1 到 64 個字元 |
p.nickname | string | 否 | 暱稱,最長 64 個字元 |
返回值:Promise<SelfProfile>,新使用者的資料。
可能的錯誤:permission_denied(應用不允許客戶端註冊)、already_exists(使用者名稱已被佔用)、invalid_argument(引數不符合規則)、content_rejected(暱稱未透過內容安全檢查)、limit_exceeded(應用的註冊使用者數已滿)、rate_limited。
im.auth.logout()
退出登入。本地立即生效,同一瀏覽器的其他標籤頁一起退出。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.clearLocalData | boolean | 否 | 是否同時刪除這個使用者的本地資料,預設 false |
返回值:Promise<void>。通知服務端失敗時不拒絕,SDK 稍後補發。未登入時直接兌現。
im.auth.changePassword()
修改本人的密碼。成功後其他裝置下線,本裝置保持登入。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.current_password | string | 是 | 當前密碼 |
p.new_password | string | 是 | 新密碼,1 到 64 個字元 |
返回值:Promise<void>。
可能的錯誤:invalid_credentials(當前密碼錯誤)、too_many_attempts(錯誤次數過多)、permission_denied(沒有設定過密碼)、invalid_argument(新密碼不符合規則)、not_signed_in。
im.auth.clearLocalData()
刪除指定使用者在本地的資料(會話、訊息、聯絡人等快取),不影響服務端的資料和登入狀態。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.username | string | 否 | 要刪除的使用者。不能是當前登入的使用者 |
p.all | boolean | 否 | 刪除本應用的全部本地資料,只能在未登入時使用 |
兩者給出一個。
返回值:Promise<void>。
可能的錯誤:invalid_state(details.reason 為 signed_in:指定了當前登入的使用者,或已登入時使用 all)、local_validation(兩者都沒有給出)。
im.auth.state
只讀屬性,當前的登入狀態 AuthState,同步讀取。變化時發出 auth.stateChanged。
im.auth.currentUser
只讀屬性,當前登入的使用者 { username: string; session_id: string },未登入或會話已結束時為 null。服務暫停期間仍有值。username 為小寫。
im.connection.state
只讀屬性,當前的連線狀態 ConnectionState。變化時發出 connection.stateChanged。
im.connection.isLeader
只讀屬性,boolean,本標籤頁是否為主標籤頁。變化時發出 tabs.roleChanged。
im.connection.isAlertTab
只讀屬性,boolean,本標籤頁當前是否負責提醒。變化時發出 tabs.roleChanged。彈出通知時以事件載荷中的 alert 為準。
im.connection.serverTimeOffsetMs
只讀屬性,number,服務端時間減本地時間的毫秒數,連線後更新。需要按服務端時間顯示倒計時(如禁言到期)時,用 Date.now() + im.connection.serverTimeOffsetMs 作為當前時間。
im.connection.reconnectNow()
立即重連,不等退避的間隔結束(服務端要求的等待時間除外)。只在主標籤頁中起作用,連線已停止(stopped)時不起作用。
返回值:無。
im.connection.waitUntilReady()
等到連線狀態為 ready。已經是 ready 時立即兌現。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
p.timeoutMs | number | 否 | 最長等待的毫秒數,預設一直等待 |
p.signal | AbortSignal | 否 | 用於取消等待 |
返回值:Promise<void>。
可能的錯誤:timeout(超時)、aborted(被取消)。
im.connection.getDiagnostics()
返回診斷資訊。
返回值:Diagnostics。
im.config
只讀屬性,ClientConfig | null,應用的執行配置,未登入時為 null。變化時發出 config.changed。
im.on()
訂閱事件,返回取消訂閱的函式。見事件與錯誤處理。
im.once()
訂閱事件,只觸發一次,返回取消訂閱的函式。
im.destroy()
釋放客戶端:關閉長連線、釋放全部列表、不再發出事件。不退出登入。可以重複呼叫,返回同一個 Promise。
返回值:Promise<void>。
im.hasPendingWork()
是否還有沒發出的訊息或沒完成的上傳。
返回值:boolean。
資料結構
LoginResult
| 欄位 | 型別 | 說明 |
|---|---|---|
user | SelfProfile | 本人資料:username、nickname、avatar_url、attributes、has_password、version,見使用者、好友與線上狀態 |
config | ClientConfig | 應用的執行配置,與 im.config 相同 |
session_id | string | 本次登入的會話 ID |
AuthState
kind | 其他欄位 | 說明 |
|---|---|---|
signed_out | 未登入 | |
signed_in | 已登入 | |
suspended | code:'tenant_unavailable' | 'app_unavailable';reason?:string;retryAt:number | 服務暫停,見服務暫停 |
ended | code:string;reason?:string;details?:Record<string, unknown> | 會話已結束,見會話結束 |
ConnectionState
kind | 其他欄位 | 說明 |
|---|---|---|
idle | 未登入或未開始連線 | |
connecting | 正在連線 | |
authenticating | 正在認證 | |
syncing | pending:SyncModule[] | 正在同步,pending 為還沒完成的模組:config、me、friends、groups、messages、push、calls、chatrooms、presence |
ready | 已就緒 | |
waiting | retryAt:number;lastError?:{ code: string; reason?: string } | 等待重連 |
offline | 網路不可用 | |
stopped | code:'upgrade_required' | 'replaced_elsewhere' | 'refresh_required' | 'invalid_auth';details?:Record<string, unknown> | 已停止,見連線狀態 |
TicketContext
ticketProvider 的引數。
| 欄位 | 型別 | 說明 |
|---|---|---|
reason | 'invalid_credentials' | 'session_expired' | invalid_credentials:憑證登入失敗;session_expired:登入已過期,靜默重新登入 |
username | string | null | 登入過期時為原來的使用者名稱;憑證登入失敗時為呼叫 loginWithTicket 時傳入的 username,沒有傳時為 null |
Diagnostics
| 欄位 | 型別 | 說明 |
|---|---|---|
sdkVersion | string | SDK 的版本號 |
deviceId | string | 裝置標識 |
sessionId | string | null | 當前登入的會話 ID |
connectionId | string | null | 當前長連線的 ID |
isLeader | boolean | 本標籤頁是否為主標籤頁 |
connectionState | ConnectionState | 連線狀態 |
recentDisconnects | Array<{ at: number; closeCode: number | null; code?: string; reason?: string }> | 最近 20 次斷開的時間、關閉碼和原因 |
recentFailures | Array<{ at: number; op: string; code: string; requestId?: string }> | 最近 20 個失敗的請求 |
loginMethod | 'ticket' | 'password' | null | 登入方式 |
lastSyncAt | Partial<Record<SyncModule, number>> | 各模組最近一次同步完成的時間 |
storageMode | 'indexeddb' | 'memory' | 本地資料儲存在哪裡 |
degraded | boolean | 是否處於降級模式(如 HTTP 頁面) |
ClientConfig
應用的執行配置,欄位與服務端客戶端登入響應中的 config 相同,常用欄位見執行配置。另有 version(string),為配置的版本。
LogEntry
| 欄位 | 型別 | 說明 |
|---|---|---|
level | 'error' | 'warn' | 'info' | 'debug' | 級別 |
time | number | 毫秒時間戳 |
module | string | SDK 內部的模組名,如 connection、auth、messages |
message | string | 日誌內容 |
code | string | 錯誤碼,可選 |
reason | string | 錯誤原因,可選 |
requestId | string | 請求的 request_id,可選 |
connectionId | string | 長連線的 ID,可選 |
頻道能力與登入生命週期
使用獨立頻道前要同時滿足兩個條件:
| 檢查 | 表示 |
|---|---|
im.config?.rtc_channels_enabled === true | 應用已在服務端開通頻道。rtc_channels_protocol_version 為頻道協議版本,當前為 1 |
im.channels.supported === true | 當前瀏覽器有加入頻道所需的媒體能力 |
- 頻道介面只在包含它的 SDK 版本中提供:舊版本的 SDK 即使應用開通了頻道,也沒有
im.channels。 - 頻道使用當前登入的 IM 身份。加入、離開等操作走 HTTP 請求,長連線只用於更快地收到狀態變化。
- 退出登入、切換使用者或呼叫
im.destroy()時,SDK 自動離開頻道、停止攝像頭和麥克風;新登入的使用者不能接著使用之前的頻道。 - 只是離開頁面或解除安裝元件不會離開頻道:請在離開前呼叫
leave(),不要只移除影片元素。
