可靠穩固的鏈接,無感的自動驗證、數據同步、多終端同步數據,並保障用戶數據安全、隱私,打造與 Telegram
同樣專一於 IM
的應用。因此保障與服務端的可靠鏈接是最重要的事情之一。git
little-chat 擁有上述特色,是體驗更好的 IM 客戶端。github
websocket 鏈接並不可靠,想要創建穩定可靠的 websocket
連接,最理想的是在 onclose
或 onerr
的回調中嘗試作重連。web
但這兩個回調有時候並不可靠,特別在移動端,當瀏覽器被退到後臺運行時,即便斷開
或異常
也未必觸發 onclose
或 onerr
回調。api
那如何保證 websocket 正確地、持續地與服務端鏈接?瀏覽器
瀏覽器的 DOM
提供了一個 visibilitychange
事件,因而咱們即可以經過監聽它判斷 document.hidden
獲知當前頁面是否退到後臺運行。緩存
function handleVisibilityChange() { const isPageBeHide = document.hidden; if(isPageBeHide) { // ...do something } } document.addEventListener("visibilitychange", handleVisibilityChange);
通過測試,visibilitychange
事件在在 Chrome, Safari, Firefox 等主流瀏覽器都是起做用的,包括 Android 和 iOS 終端下的 webview,因此 React-Native 封裝的 webview 也適用。安全
因而能夠嘗試這樣的機制:websocket
let $WS // 啓動 websocket function initWS() { if(!$WS) { $WS = new websocket('ws://api'); $WS.onopen = () => { } $WS.onclose = () => { } $WS.onerror = () => { } } } // 關閉 websocket function closeWS() { if($WS) { // 主動斷開 $WS.close(); // 清除 websocket 實例 $WS = null; } } // 重連 websocket function reconnect() { closeWS(); initWS(); } // 應用啓動時 initWS(); function handleVisibilityChange() { const isPageBeHide = document.hidden; if(isPageBeHide) { closeWS(); } else { reconnect(); } } document.addEventListener("visibilitychange", handleVisibilityChange);
上述能夠主動掌握鏈接,但這不能確保 $WS.send()
會在 onopen
後執行,畢竟這操做的主動權在應用業務中,因此咱們還須要確保消息必定在 onopen
以後發送。session
如下爲基礎 websocket 封裝類,設置了 unSendQueue
來保存未鏈接成功時的請求,具體以下(被隱去了不少細節):socket
import { EventEmitter, EventEmitterClass, Call } from 'basic-helper'; const onOpenMark = 'onOpen'; const onMessageMark = 'onMessage'; function wrapWSUrl(hostname) { if (!/wss?:\/\//.test(hostname)) { console.warn('websocket host 不正確', hostname); } return hostname; } class SocketHelper extends EventEmitterClass { // ... 被隱去的細節 // 未鏈接成功前發起的請求 unSendQueue: UnSendEntity = {}; permissionsQueue: UnSendEntity = {}; constructor(params: SocketParams) { super(); this.params = params; this.initWS(); } initWS = () => { if (this.connecting) return; this.connecting = true; const { apiHost } = this.params; if (!apiHost) { console.error('請傳入 apiHost'); return; } const wsApiHost = wrapWSUrl(apiHost); this.socket = new WebSocket(wsApiHost); this.socket.binaryType = 'arraybuffer'; this.socket.onopen = this.onOpen; this.socket.onmessage = this.onMessage; this.socket.onerror = this.onErr; this.socket.onclose = this.onClose; } setReqQuquq = (requestID, success, fail) => { this.reqQueue[requestID.toString()] = { success, fail, }; } clearQueue = () => { this.reqQueue = {}; this.permissionsQueue = {}; } send = (sendOptions) => { const { apiName, bufData, requestID, success, fail, needAuth } = sendOptions; if (!this.connected) { /** * 若是還沒 onOpen 打開的,放入待發送隊列中 */ // console.error('還沒有鏈接'); this.unSendQueue[requestID.toString()] = sendOptions; if (!this.isClosed) this.initWS(); } else if (this.socket) { this.socket.send(data); this.setReqQuquq(requestID, success, fail); } } /** * 在 onopen 的時候發送在未 open 時候發送請求 */ sendNotComplete = (queue: UnSendEntity) => { const unSendList = Object.keys(queue); if (unSendList.length === 0) return; unSendList.forEach((requestID) => { const sendOptions = queue[requestID]; this.send(sendOptions); delete queue[requestID]; }); } onOpen = () => { // this.params.onOpen(); this.connected = true; this.connecting = false; this.emit(onOpenMark, {}); this.emit(CONNECT_READY, {}); // 在 onopen 發送未鏈接時發起的請求 this.sendNotComplete(this.unSendQueue); this.isClosed = false; } onMessage = (event) => { } onErr = (e) => { console.log('onErr'); /** 若是發生錯誤,則主動關閉 websocket 連接 */ this.socket && this.socket.close(); } onClose = (e) => { console.log('onClose'); this.handleException(e); } handleException = (event) => { this.connected = false; this.socket = null; this.isClosed = true; this.clearQueue(); EventEmitter.emit(ON_CONNECT_CLOSE, event); } } export default SocketHelper;
如下爲基於 SocketHelper
的更進一步的封裝(讓 API 的用法與 HTTP
一致):
import SocketHelper from './socket'; let $WS; let prevWSParams; function GetWS() { if (!$WS) console.error(SDKErrorDesc); return $WS; } function WSSend<T extends Api, S>(api: T, apiName: string, data?, needAuth = true): Promise<S> { return new Promise((resolve, reject) => { if (!$WS) { console.error(SDKErrorDesc); return reject(SDKErrorDesc); } const requestID = BigInt(UUID(16)); const msgWrite = api.create(data); const bufData = api.encode(msgWrite).finish(); // const finalData = encodeData(apiName, bufData, requestID); $WS.send({ apiName, bufData, requestID, success: (res) => { resolve(res); }, fail: (res) => { failResHandler(res); reject(res); }, needAuth }); }); } function InitSDK(params: Params = prevWSParams) { /** 保存上一個參數 */ if (params) prevWSParams = params; const { apiHost } = params; $WS = new SocketHelper({ apiHost }); return $WS; } /** * 檢查是否正常連接 */ function CheckConnectState() { let isConnecting = false; if (!$WS) return isConnecting; isConnecting = $WS.connected; return isConnecting; } /** * 關閉 websocket 連接 */ function CloseWS() { if ($WS) { if ($WS.socket) $WS.socket.close(); $WS = null; } } export { InitSDK, GetWS, WSSend, CheckConnectState, CloseWS };
如下爲發起請求的 API:
export async function ApplyLogin(form: IUserLoginReq) { const res = await WSSend<typeof UserLoginReq, IUserLoginResp>( UserLoginReq, 'UserLoginReq', form, false ); if (res.SessionID) { /** * 1. 成功後設置 sessionID * 2. 設置 websocket 的權限 */ setHeaderSSID(res.SessionID); GetWS().setPermissions(true); } const result = Object.assign({}, res, { UserName: form.UserName, ...res.User }); return result; }
最後在業務應層調用此 API:
const business = () => { ApplyLogin({ // ... }) .then((res) => { // ... }) }
固然還有一個問題是,有少部分請求能夠不帶 session,例如登錄,可是其餘請求須要,這個須要在 SocketHelper 中再作進一步的驗證封裝,在未驗證經過時,把須要驗證的請求緩存到隊列,而後鏈接成功而且驗證成功後再發送,這樣能夠達到無感登錄地數據同步
的體驗。
詳情參考 little-chat