[TOC]
本文介紹 JavaScript 實時通訊 SDK version 3 的使用,version 2 的文檔請參考[《JavaScript 實時通信開發指南(version 2)》](https://leancloud.cn/docs/js_realtime.html)。
## [簡介](#簡介)
實時通信服務可以讓你一行后端代碼都不用寫,就能做出一個功能完備的實時聊天應用,或是一個實時對戰類的游戲。所有聊天記錄都保存在云端,離線消息會通過消息推送來及時送達,推送消息文本可以靈活進行定制。
> 在繼續閱讀本文檔之前,請先閱讀[《實時通信開發指南》](https://leancloud.cn/docs/realtime_v2.html),了解一下實時通信的基本概念和模型。
>
>
>
> +
>
>
### [兼容性](#兼容性)
JavaScript 實時通信 SDK 支持如下運行時:
* 瀏覽器/WebView
* IE 10+ / Edge
* Chrome 31+
* Firefox latest
* iOS 8.0+
* Android 4.4+
* Node.js 0.12+
* React Native 0.26+
* 微信小程序開發者工具 latest(參見?[在微信小程序中使用 LeanCloud](https://leancloud.cn/docs/weapp.html))
### [文檔貢獻](#文檔貢獻)
我們歡迎和鼓勵大家對本文檔的不足提出修改建議。請訪問我們的?[Github 文檔倉庫](https://github.com/leancloud/docs)?來提交 Pull Request。
### [API 文檔](#API_文檔)
[https://leancloud.github.io/js-realtime-sdk/docs/](https://leancloud.github.io/js-realtime-sdk/docs/)
## [安裝和初始化](#安裝和初始化)
### [安裝](#安裝)
建議使用 npm 安裝 SDK,在終端運行以下命令:
~~~
npm install leancloud-realtime --save
~~~
### [引用](#引用)
SDK 暴露(export)了以下成員:[SDK API 文檔](https://leancloud.github.io/js-realtime-sdk/docs/module-leancloud-realtime.html)。
如果是在瀏覽器中使用,需要加載以下 script:
~~~
<script src="./node_modules/leancloud-realtime/dist/realtime.browser.js"></script>
~~~
在瀏覽器中直接加載時,SDK 暴露的所有的成員都掛載在?`AV`?命名空間下:
~~~
var Realtime = AV.Realtime;
var TextMessage = AV.TextMessage;
~~~
如果是在 Node.js 或其他支持 CommonJS 模塊規范的環境中使用,需要按以下方法進行 require:
~~~
var Realtime = require('leancloud-realtime').Realtime;
var TextMessage = require('leancloud-realtime').TextMessage;
~~~
### [初始化](#初始化)
按照上面的方式拿到?`Realtime`?類后,可以按照下面用法初始化一個?`realtime`?實例,在下面的文檔中如果出現了未定義的?`realtime`?指的均是這個實例。
~~~
var realtime = new Realtime({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
region: 'cn', //美國節點為 "us"
});
~~~
在微信小程序中使用時需要在初始化時指定 noBinary 參數為 true:
~~~
const realtime = new Realtime({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
region: 'cn', // 美國節點為 "us"
noBinary: true,
});
~~~
### [富媒體消息插件](#富媒體消息插件)
如果需要使用?[富媒體消息](#富媒體消息)?中的?`ImageMessage`、`AudioMessage`、`VideoMessage`、`FileMessage`?或?`LocationMessage`,需要額外安裝 leancloud-realtime-plugin-typed-messages 與 leancloud-storage:
~~~
npm install --save leancloud-realtime-plugin-typed-messages leancloud-storage
~~~
在瀏覽器中使用時按照以下順序加載:
~~~
<script src="./node_modules/leancloud-storage/dist/av.js"></script>
<script src="./node_modules/leancloud-realtime/dist/realtime.browser.js"></script>
<script src="./node_modules/leancloud-realtime-plugin-typed-messages/dist/typed-messages.js"></script>
~~~
然后依次進行初始化:
~~~
// 初始化存儲 SDK
AV.init({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
appKey:'K2CE4ChmGnUwI8mMBgTRHw7y',
});
// 初始化實時通訊 SDK
var Realtime = AV.Realtime;
var realtime = new Realtime({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
plugins: [AV.TypedMessagesPlugin], // 注冊富媒體消息插件
});
// 在瀏覽器中直接加載時,富媒體消息插件暴露的所有的成員都掛載在 AV 命名空間下
var imageMessage = new AV.ImageMessage(file);
~~~
如果是在 Node.js 或其他支持 CommonJS 模塊規范的環境中使用,需要按以下方法進行引用與初始化:
~~~
var AV = require('leancloud-storage');
var Realtime = require('leancloud-realtime').Realtime;
var TypedMessagesPlugin = require('leancloud-realtime-plugin-typed-messages').TypedMessagesPlugin;
var ImageMessage = require('leancloud-realtime-plugin-typed-messages').ImageMessage;
// 初始化存儲 SDK
AV.init({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
appKey:'K2CE4ChmGnUwI8mMBgTRHw7y',
});
// 初始化實時通訊 SDK
var realtime = new Realtime({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
plugins: [TypedMessagesPlugin], // 注冊富媒體消息插件
});
var imageMessage = new ImageMessage(file);
~~~
富媒體消息插件暴露(export)的成員完整列表請參見:?[富媒體消息插件 API 文檔](https://leancloud.github.io/js-realtime-sdk/plugins/typed-messages/docs/module-leancloud-realtime-plugin-typed-messages.html)
## [單聊](#單聊)
我們先從最簡單的環節入手。此場景類似于微信的私聊、微博的私信和 QQ 單聊。我們創建了一個統一的概念來描述聊天的各種場景:Conversation(對話),在[《實時通信開發指南》](https://leancloud.cn/docs/realtime_v2.html)?里也有相關的詳細介紹。
### [發送消息](#發送消息)

Tom 想給 Jerry 發一條消息,實現代碼如下:
~~~
// Tom 用自己的名字作為 clientId,獲取 IMClient 對象實例
realtime.createIMClient('Tom').then(function(tom) {
// 創建與Jerry之間的對話
return tom.createConversation({
members: ['Jerry'],
name: 'Tom & Jerry',
});
}).then(function(conversation) {
// 發送消息
return conversation.send(new AV.TextMessage('耗子,起床!'));
}).then(function(message) {
console.log('Tom & Jerry', '發送成功!');
}).catch(console.error);
~~~
執行完以上代碼,在 LeanCloud 網站的?[控制臺 /(選擇應用)/ 存儲 / 數據 /?`_Conversation`?表](https://leancloud.cn/data.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/_Conversation)?中多了一行數據,其字段含義如下:
2
| 名稱 | 類型 | 描述 |
| --- | --- | --- |
| name | String | 對話唯一的名字 |
| m | Array | 對話中成員的列表 |
| lm | Date | 對話中最后一條消息發送的時間 |
| c | String | 對話的創建者的 ClientId |
| mu | Array | 對話中設置了靜音的成員,僅針對 iOS 以及 Windows Phone 用戶有效。 |
> 提示:每次調用?`createConversation()`?方法,都會生成一個新的 Conversation 實例,即便使用相同 members 和 name 也是如此。如果想要不重復創建相同成員的對話,請參閱?[常見問題](#常見問題)?。
>
>
>
> +
>
>
### [接收消息](#接收消息)
要讓 Jerry 收到 Tom 的消息,需要這樣寫:
~~~
// Jerry 登錄
realtime.createIMClient('Jerry').then(function(jerry) {
jerry.on('message', function(message, conversation) {
console.log('Message received: ' + message.text);
});
}).catch(console.error);
~~~
## [群聊](#群聊)
對于多人同時參與的固定群組,我們有成員人數限制,最大不能超過 500 人。對于另外一種多人聊天的形式,譬如聊天室,其成員不固定,用戶可以隨意進入發言的這種「臨時性」群組,后面會單獨介紹。
0+
### [發送消息](#發送消息-1)
Tom 想建立一個群,把自己好朋友都拉進這個群,然后給他們發消息,他需要做的事情是:
1. 建立一個朋友列表
2. 新建一個對話,把朋友們列為對話的參與人員
3. 發送消息
~~~
// Tom 用自己的名字作為 clientId,獲取 Client 對象實例
realtime.createIMClient('Tom').then(function(tom) {
// 創建與 Jerry,Bob,Harry,William 之間的對話
return tom.createConversation({
members: ['Jerry', 'Bob', 'Harry', 'William'],
name: 'Tom & Jerry & friends',
})
}).then(function(conversation) {
// 發送消息
return conversation.send(new AV.TextMessage('你們在哪兒?'));
}).then(function(message) {
console.log('發送成功!');
}).catch(console.error);
~~~
### [接收消息](#接收消息-1)
群聊的接收消息與單聊的接收消息在代碼寫法上是一致的。
1
~~~
// Bob 登錄
realtime.createIMClient('Bob').then(function(bob) {
bob.on('message', function(message, conversation) {
console.log('[Bob] received a message from [' + message.from + ']: ' + message.text);
// 收到消息之后一般的做法是做 UI 展現,示例代碼在此處做消息回復,僅為了演示收到消息之后的操作,僅供參考。
conversation.send(new AV.TextMessage('Tom,我在 Jerry 家,你跟 Harry 什么時候過來?還有 William 和你在一起么?'));
});
}).catch(console.error);
// William 登錄
realtime.createIMClient('William').then(function(william) {
william.on('message', function(message, conversation) {
console.log('[William] received a message from [' + message.from + ']: ' + message.text);
});
}).catch(console.error);
~~~
以上由 Tom 和 Bob 發送的消息,William 在上線時都會收到。
由此可以看出,群聊和單聊本質上都是對話,只是參與人數不同。單聊是一對一的對話,群聊是多對多的對話。
用戶在開始聊天之前,需要先登錄 LeanCloud 云端。這個登錄并不需要用戶名和密碼認證,只是與 LeanCloud 云端建立一個長連接,所以只需要傳入一個唯一標識作為當前用戶的?`clientId`?即可。
2
為直觀起見,我們使用了 Tom、Jerry 等字符串作為 clientId 登錄聊天系統。LeanCloud 云端只要求 clientId 在應用內唯一、不超過 64 個字符的字符串即可,具體用什么數據由應用層決定。
實時通信 SDK 在內部會為每一個 clientId 創建唯一的?`Client`?實例,也就是說多次使用相同的 clientId 創建出來的實例還是同一個。因此,如果要支持同一個客戶端內多賬號登錄,只要使用不同的 clientId 來創建多個實例即可。我們的 SDK 也支持多賬戶同時登錄。
## [消息](#消息)
消息是一個對話的基本組成部分,我們支持的消息類型有:
* 文本消息:`TextMessage`
* 圖像消息:`ImageMessage`
* 音頻消息:`AudioMessage`
* 視頻消息:`VideoMessage`
* 文件消息:`FileMessage`
* 位置消息:`LocationMessage`
除了?`TextMessage`?已經內置,其他的消息類型需要額外安裝插件 leancloud-realtime-plugin-typed-messages,具體的安裝與初始化方法參見?[安裝 - 富媒體消息插件](#富媒體消息插件)。
### [富媒體消息](#富媒體消息)
#### [發送消息](#發送消息-2)
##### 圖像消息、音頻消息、視頻消息、文件消息
圖像可以通過瀏覽器或 Node.js 提供的 API 獲取,也可以用有效的圖像 URL。先使用存儲 SDK 的?`AV.File`?類?[構造出一個文件對象](https://leancloud.cn/docs/leanstorage_guide-js.html#文件),再調用其?`save`?方法將其保存到服務端,然后把它當做參數構造一個?`ImageMessage`?的實例,最后通過?`Conversation#send`?方法即可發送這條消息。
音頻消息、視頻消息、文件消息的構造與發送與圖像消息類似,不再贅述。
###### 發送圖像消息
【場景一】用瀏覽器提供的 API 去獲取本地的照片,然后構造出?`ImageMessage`?來發送:
~~~
/* html: <input type="file" id="photoFileUpload"> */
var fileUploadControl = $('#photoFileUpload')[0];
var file = new AV.File('avatar.jpg', fileUploadControl.files[0]);
file.save().then(function() {
var message = new AV.ImageMessage(file);
message.setText('發自我的小米');
message.setAttributes({ location: '舊金山' });
return conversation.send(message);
}).then(function() {
console.log('發送成功');
}).catch(console.error.bind(console));
~~~
【場景二】從微博上復制的一個圖像鏈接來創建圖像消息:
~~~
var file = new AV.File.withURL('萌妹子', 'http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif');
file.save().then(function() {
var message = new AV.ImageMessage(file);
message.setText('萌妹子一枚');
return conversation.send(message);
}).then(function() {
console.log('發送成功');
}).catch(console.error.bind(console));
~~~
以上兩種場景對于 SDK 的區別為:
* 場景一:SDK 獲取了完整的圖像數據流,先上傳文件到云端,再將文件 URL 包裝在消息體內發送出去。
* 場景二:SDK 并沒有將圖像實際上傳到云端,而僅僅把 URL 包裝在消息體內發送出去。
需要特別指出,與其他 SDK 不同的是,由于 JavaScript 存儲 SDK 在處理文件時不會自動獲取圖像文件的大小、寬高等元信息,在默認的情況下情況下接收方是無法從消息體中獲取圖像的元信息數據,但是接收方可以自行通過客戶端技術去分析圖片的格式、大小、長寬之類的元數據。或者你也可以通過其他方式獲得圖像的元信息,然后通過?`AV.File#metaData`?方法手動設置這些信息。
##### 地理位置消息
先使用存儲 SDK 的?`AV.GeoPoint`?類?[構造出一個地理位置對象](https://leancloud.cn/docs/leanstorage_guide-js.html#地理位置),然后把它當做參數構造一個?`LocationMessage`?的實例,最后通過?`Conversation#send`?方法即可發送這條消息。
~~~
var location = new AV.GeoPoint(31.3753285,120.9664658);
var message = new AV.LocationMessage(location);
message.setText('新開的蛋糕店!耗子咱們有福了…');
conversation.send(message).then(function() {
console.log('發送成功');
}).catch(console.error.bind(console));
~~~
#### [接收富媒體消息](#接收富媒體消息)
實時通信 SDK 提供的所有富媒體消息類都是從 TypedMessage 派生出來的。發送的時候可以直接調用?`conversation.send()`?函數。在接收端,SDK 會在 IMClient 實例上派發?`message`?事件,接收端處理富媒體消息的示例代碼如下:
~~~
// 在初始化 Realtime 時,需加載 TypedMessagesPlugin
// var realtime = new Realtime({
// appId: appId,
// plugins: [AV.TypedMessagesPlugin,]
// });
// 注冊 message 事件的 handler
client.on('message', function messageEventHandler(message, conversation) {
// 請按自己需求改寫
var file;
switch (message.type) {
case AV.TextMessage.TYPE:
console.log('收到文本消息, text: ' + message.getText() + ', msgId: ' + message.id);
break;
case AV.FileMessage.TYPE:
file = message.getFile(); // file 是 AV.File 實例
console.log('收到文件消息,url: ' + file.url() + ', size: ' + file.metaData('size'));
break;
case AV.ImageMessage.TYPE:
file = message.getFile();
console.log('收到圖片消息,url: ' + file.url() + ', width: ' + file.metaData('width'));
break;
case AV.AudioMessage.TYPE:
file = message.getFile();
console.log('收到音頻消息,url: ' + file.url() + ', width: ' + file.metaData('duration'));
break;
case AV.VideoMessage.TYPE:
file = message.getFile();
console.log('收到視頻消息,url: ' + file.url() + ', width: ' + file.metaData('duration'));
break;
case AV.LocationMessage.TYPE:
var location = message.getLocation();
console.log('收到位置消息,latitude: ' + location.latitude + ', longitude: ' + location.longitude);
break;
default:
console.warn('收到未知類型消息');
}
});
~~~
同時,對應的 conversation 上也會派發?`message`?事件:
~~~
conversation.on('message', function messageEventHandler(message) {
// your logic
});
~~~
### [消息發送選項](#消息發送選項)
消息發送選項用于在發送消息時定義消息的一些特性。包含以下特性:
#### [消息等級](#消息等級)
為了保證消息的時效性,當聊天室消息過多導致客戶端連接堵塞時,服務器端會選擇性地丟棄部分低等級的消息。目前支持的消息等級有:
| 消息等級 | 描述 |
| --- | --- |
| `MessagePriority.HIGH` | 高等級,針對時效性要求較高的消息,比如直播聊天室中的禮物,打賞等。 |
| `MessagePriority.NORMAL` | 正常等級,比如普通非重復性的文本消息。 |
| `MessagePriority.LOW` | 低等級,針對時效性要求較低的消息,比如直播聊天室中的彈幕。 |
消息等級在發送接口的參數中設置。以下代碼演示了如何發送一個高等級的消息:
~~~
var realtime = new Realtime({ appId: '', region: 'cn' });
realtime.createIMClient('host').then(function (host) {
return host.createConversation({
members: ['broadcast'],
name: '2094 世界杯決賽梵蒂岡對陣中國比賽直播間',
transient: true
});
}).then(function (conversation) {
console.log(conversation.id);
return conversation.send(new AV.TextMessage('現在比分是 0:0,下半場中國隊肯定要做出人員調整'), { priority: AV.MessagePriority.HIGH });
}).then(function (message) {
console.log(message);
}).catch(console.error);
~~~
此功能僅針對聊天室消息有效。普通對話的消息不需要設置等級,即使設置了也會被系統忽略,因為普通對話的消息不會被丟棄。
#### [暫態消息](#暫態消息)
暫態消息不會被自動保存(以后在歷史消息中無法找到它),也不支持延遲接收,離線用戶更不會收到推送通知,所以適合用來做控制協議。譬如聊天過程中「某某正在輸入...」這樣的狀態信息,就適合通過暫態消息來發送;或者當群聊的名稱修改以后,也可以用暫態消息來通知該群的成員「群名稱被某某修改為...」。
~~~
// operation-message.js
var { TypedMessage, messageType, messageField } = require('leancloud-realtime');
// 自定義的消息類型,用于發送和接收所有的用戶操作消息
// 這里使用了 TypeScript 的語法,也可以使用其他的繼承機制的實現,詳見「自定義消息類型」章節
// 指定 type 類型,可以根據實際換成其他正整數
@messageType(1)
@messageField('op')
class OperationMessage extends TypedMessage {}
// app.js
realtime.createIMClient('tom').then(function(tom) {
return tom.createConversation({
members: ['bob'],
});
}).then(function(conversation) {
var message = new OperationMessage();
message.op = 'typing';
// 設置該條消息為暫態消息
message.setTransient(true);
return conversation.send(message);
}).then(function() {
console.log('發送成功');
}).catch(console.error.bind(console));
~~~
而對話中的其他成員在程序中需要有以下代碼做出響應:
~~~
// operation-message.js 同發送
// app.js
// 首先需要注冊自定義消息類型
realtime.register(OperationMessage);
realtime.createIMClient('bob').then(function(bob) {
// 注冊 message 事件的 handler
client.on('message', function messageEventHandler(message, conversation) {
switch (message.type) {
case OperationMessage.TYPE:
console.log(message.from + ' is ' + message.op);
break;
// case ...
default:
console.warn('收到未知類型消息');
}
});
});
~~~
#### [消息送達回執](#消息送達回執)
是指消息被對方收到之后,云端會發送一個回執通知給發送方,表明消息已經送達。
發送時標記消息為「需要回執」:
~~~
var message = new AV.TextMessage('very important message');
conversation.send(message, {
reciept: true,
});
~~~
當消息的接收方收到消息后,服務端會通知消息的發送方「消息已送達」,發送方的 SDK 會在 conversation 上派發一個?`receipt`?事件:
~~~
conversation.on('receipt', function(payload) {
// payload.message 為送達的消息,與先前發送的是同一實例
// message.status 更新為 MessageStatus.DELIVERED
// message.deliveredAt 為消息送達的時間
console.log(payload.message);
});
~~~
需要注意的是:
> 只有在發送時設置了「需要回執」標記,云端才會發送回執,默認不發送回執。該回執并不代表用戶已讀。
>
>
>
> +
>
>
#### [自定義離線推送內容](#自定義離線推送內容)
發送消息時,可以指定該消息對應的離線推送內容。如果消息接收方不在線,我們會推送您指定的內容。以下代碼演示了如何自定義離線推送內容:
~~~
var realtime = new Realtime({ appId: '', region: 'cn' });
realtime.createIMClient('Tom').then(function (host) {
return host.createConversation({
members: ['Jerry'],
name: 'Tom & Jerry',
unique: true
});
}).then(function (conversation) {
console.log(conversation.id);
return conversation.send(new AV.TextMessage('耗子,今晚有比賽,我約了 Kate,咱們仨一起去酒吧看比賽啊?!'), {
pushData: {
"data": {
"alert": "您有一條未讀的消息",
"category": "消息",
"badge": 1,
"sound": "聲音文件名,前提在應用里存在",
"custom-key": "由用戶添加的自定義屬性,custom-key 僅是舉例,可隨意替換"
}
}
});
}).then(function (message) {
console.log(message);
}).catch(console.error);
~~~
除此以外,還有其他方法來自定義離線推送內容,請參考?[實時通信概覽 · 離線推送通知](https://leancloud.cn/docs/realtime_v2.html#離線推送通知)。
### [未讀消息](#未讀消息)
未讀消息有兩種處理方式,未讀消息數量通知與離線消息通知。
#### [未讀消息數量通知](#未讀消息數量通知)
未讀消息數量通知是默認的未讀消息處理方式:當客戶端上線時,會收到其參與過的會話的未讀消息數量的通知,然后由客戶端負責主動拉取未讀的消息并手動標記為已讀。
當收到未讀消息數量通知時,SDK 會在 Client 上派發?`unreadmessages`?事件。
~~~
client.on('unreadmessages', function unreadMessagesEventHandler(payload, conversation) {
console.log(payload);
// {
// count: 4,
// lastMessageId: "UagNXHK0RHqIvM_VB7Injg",
// lastMessageTimestamp: [object Date],
// }
})
~~~
如果有多個對話有未讀消息,這個事件會被派發多次,對應的 conversation 的未讀消息數(`conversation.unreadMessagesCount`)會自動更新,此時開發者可以在對話列表界面上更新這些對話的未讀消息數量。
當用戶點擊進入某個對話時,開發者需要做兩件事:
1. 拉取消息記錄,參見[聊天記錄](#聊天記錄)
2. 調用?`Conversation#markAsRead`?標記該會話為已讀:
~~~
conversation.markAsRead().then(function(conversation) {
console.log('對話已標記為已讀');
}).catch(console.error.bind(console));
~~~
此時,當前用戶其他在線的客戶端會收到?`unreadmessages`?消息,將該會話的未讀消息數更新為 0。
除了?`Conversation#markAsRead`,SDK 還提供了?`IMClient#markAllAsRead`?方法來批量標記對話為已讀:
1
~~~
client.markAllAsRead([conversation]).then(function() {
console.log('對話已全部標記已讀');
}).catch(console.error.bind(console));
~~~
#### [離線消息通知](#離線消息通知)
離線消息通知方式是指,當客戶端上線時,服務器會主動將所有離線時收到的消息推送過來,每個對話最多推送 20 條最近的消息。當收到離線消息時,SDK 會在 Client 上派發?`messages`?事件,與在線時收到消息無異。
要使用離線消息通知方式,需要在初始化 Realtime 時設置參數?`pushOfflineMessages`?為?`true`:
~~~
var realtime = new AV.Realtime({
appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz',
pushOfflineMessages: true,
});
~~~
### [消息類詳解](#消息類詳解)
消息類型之間的關系

消息類均包含以下屬性:
| 屬性 | 類型 | 描述 |
| --- | --- | --- |
| from | String | 消息發送者的 clientId |
| cid | String | 消息所屬對話 id |
| id | String | 消息發送成功之后,由 LeanCloud 云端給每條消息賦予的唯一 id |
| timestamp | Date | 消息發送的時間。消息發送成功之后,由 LeanCloud 云端賦予的全局的時間戳。 |
| deliveredAt | Date | 消息送達時間 |
| status | Symbol | 消息狀態,其值為枚舉?[`MessageStatus`](https://leancloud.github.io/js-realtime-sdk/docs/module-leancloud-realtime.html#.MessageStatus)?的成員之一:
`MessageStatus.NONE`(未知)
`MessageStatus.SENDING`(發送中)
`MessageStatus.SENT`(發送成功)
`MessageStatus.DELIVERED`(已送達)
`MessageStatus.FAILED`(失敗) |
我們為每一種富媒體消息定義了一個消息類型,實時通信 SDK 自身使用的類型是負數(如下面列表所示),所有正數留給開發者自定義擴展類型使用,0 作為「沒有類型」被保留起來。
1
| 消息 | 類型 |
| --- | --- |
| 文本消息 | -1 |
| 圖像消息 | -2 |
| 音頻消息 | -3 |
| 視頻消息 | -4 |
| 位置消息 | -5 |
| 文件消息 | -6 |
### [自定義消息](#自定義消息)
在某些場景下,開發者需要在發送消息時附帶上自己業務邏輯需求的自定義屬性,比如消息發送的設備名稱,或是圖像消息的拍攝地點、視頻消息的來源等等,開發者可以通過 實現這一需求。
【場景】發照片給朋友,告訴對方照片的拍攝地點:
~~~
// predefined: someAVFile, conversation
var message = new AV.ImageMessage(someAVFile);
message.setAttributes({
location: '拉薩布達拉宮',
title: '這藍天……我徹底是醉了',
});
conversation.send(message).then(function() {
console.log('發送成功');
}).catch(console.error.bind(console));
~~~
接收時可以讀取這一屬性:
~~~
// predefined: client
client.on('message', function(message) {
console.log(message.getAttributes().location); // 拉薩布達拉宮
});
~~~
所有的?`TypedMessage`?消息都支持?`attributes`?這一屬性。
#### [創建新的消息類型](#創建新的消息類型)
通過繼承 TypedMessage,開發者也可以擴展自己的富媒體消息。其要求和步驟是:
* 申明新的消息類型,繼承自 TypedMessage 或其子類,然后:
* 對 class 使用?`messageType(123)`?裝飾器,具體消息類型的值(這里是?`123`)由開發者自己決定(LeanCloud 內建的?[消息類型使用負數](#消息類詳解),所有正數都預留給開發者擴展使用)。
* 對 class 使用?`messageField(['fieldName'])`?裝飾器來聲明需要發送的字段。
* 調用?`Realtime#register()`?函數注冊這個消息類型。
舉個例子,實現一個在?[暫態消息](#暫態消息)?中提出的 OperationMessage:
~~~
// TypedMessage, messageType, messageField 都是由 leancloud-realtime 這個包提供的
// 在瀏覽器中則是 var { TypedMessage, messageType, messageField } = AV;
var { TypedMessage, messageType, messageField } = require('leancloud-realtime');
var inherit = require('inherit');
// 定義 OperationMessage 類,用于發送和接收所有的用戶操作消息
export const OperationMessage = inherit(TypedMessage);
// 指定 type 類型,可以根據實際換成其他正整數
messageType(1)(OperationMessage);
// 申明需要發送 op 字段
messageField('op')(OperationMessage);
// 注冊消息類,否則收到消息時無法自動解析為 OperationMessage
realtime.register(OperationMessage);
~~~
> 什么時候需要自己創建新的消息類型?
>
>
>
> +
>
>
>
> 譬如有一條圖像消息,除了文本之外,還需要附帶地理位置信息,為此開發者需要創建一個新的消息類型嗎?從上面的例子可以看出,其實完全沒有必要。這種情況只要使用消息類中預留的?`attributes`?屬性就可以保存額外的地理位置信息了。
>
>
>
> +
>
>
>
> 只有在我們的消息類型完全無法滿足需求的時候,才需要擴展自己的消息類型。譬如「今日頭條」里面允許用戶發送某條新聞給好友,在展示上需要新聞的標題、摘要、圖片等信息(類似于微博中的 linkcard)的話,這時候就可以擴展一個新的 NewsMessage 類。
>
>
>
> +
>
>
## [對話](#對話)
以上章節基本演示了實時通信 SDK 的核心概念「對話」,即?`Conversation`。我們將單聊和群聊(包括聊天室)的消息發送和接收都依托于?`Conversation`?這個統一的概念進行操作,所以開發者需要強化理解的一個概念就是:
> SDK 層面不區分單聊和群聊。
>
>
>
> +
>
>
對話的管理包括「成員管理」和「屬性管理」兩個方面。
在講解下面的內容之前,我們先使用 Jerry 的身份登錄并創建一個多人對話。后面的舉例中?`jerry`?指 Jerry 登錄的 client,conversation 指創建好的這個對話,CONVERSATION_ID 指這個對話的 ID。
~~~
realtime.createIMClient('Jerry').then(function(jerry) {
return jerry.createConversation({
members: ['Bob', 'Harry', 'William'],
});
}).then(function(conversation) {
var CONVERSATION_ID = conversation.id;
// now we have jerry, conversation and CONVERSATION_ID
})
~~~
### [創建對話](#創建對話)
~~~
jerry.createConversation({
members: ['Bob', 'Harry', 'William'],
name: '周末滑雪',
location: '42.86335,140.6843287',
transient: false,
unique: false,
});
~~~
參數說明:
* members - 對話的初始成員列表。在對話創建成功后,這些成員會收到和邀請加入對話一樣的相應通知。
* name - 對話的名字,主要是用于標記對話,讓用戶更好地識別對話。
* transient - 是否為?[暫態對話](#聊天室)
* unique - 是否創建唯一對話,當其為 true 時,如果當前已經有相同成員的對話存在則返回該對話,否則會創建新的對話。該值默認為 false。
option 參數中所有其他的字段(如上面例子中的?`location`)都會作為對話的自定義屬性保存。
由于暫態對話不支持創建唯一對話,所以將?`transient`?和?`unique`?同時設為 true 時并不會產生預期效果。
### [對話的成員管理](#對話的成員管理)
成員管理,是在對話中對成員的一個實時生效的操作,一旦操作成功則不可逆。
#### [成員變更接口](#成員變更接口)
成員變更操作接口簡介如下表:
| 操作目的 | 接口名 |
| --- | --- |
| 自身主動加入 | `Conversation#join` |
| 添加其他成員 | `Conversation#add` |
| 自身主動退出 | `Conversation#quit` |
| 移除其他成員 | `Conversation#remove` |
#### [成員變更事件](#成員變更事件)
成員變動之后,所有在線的對話成員,都會得到相應的通知。SDK 會在 client 上派發對應的事件:
~~~
// 有用戶被添加至某個對話
jerry.on('membersjoined', function membersjoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.invitedBy, conversation.id);
});
// 有成員被從某個對話中移除
jerry.on('membersleft', function membersleftEventHandler(payload, conversation) {
console.log(payload.members, payload.kickedBy, conversation.id);
});
// 當前用戶被添加至某個對話
jerry.on('invited', function invitedEventHandler(payload, conversation) {
console.log(payload.invitedBy, conversation.id);
});
// 當前用戶被從某個對話中移除
jerry.on('kicked', function kickedEventHandler(payload, conversation) {
console.log(payload.kickedBy, conversation.id);
});
~~~
同時在相應的 conversation 上也會派發同樣的事件:
~~~
// 有用戶被添加至某個對話
conversation.on('membersjoined', function membersjoinedEventHandler(payload) {
console.log(payload.members, payload.invitedBy);
});
// 有成員被從某個對話中移除
conversation.on('membersleft', function membersleftEventHandler(payload) {
console.log(payload.members, payload.kickedBy);
});
// 當前用戶被添加至某個對話
conversation.on('invited', function invitedEventHandler(payload) {
console.log(payload.invitedBy);
});
// 當前用戶被從某個對話中移除
conversation.on('kicked', function kickedEventHandler(payload) {
console.log(payload.kickedBy);
});
~~~
#### [添加成員](#添加成員)
##### 自身主動加入
Tom 想主動加入 Jerry、Bob、Harry 和 William 的對話,以下代碼將幫助他實現這個功能:
~~~
realtime.createIMClient('Tom').then(function(tom) {
return tom.getConversation(CONVERSATION_ID);
}).then(function(conversation) {
return conversation.join();
}).then(function(conversation) {
console.log('加入成功', conversation.members);
// 加入成功 ['Bob', 'Harry', 'William', 'Tom']
}).catch(console.error.bind(console));
~~~
##### 添加其他成員
Jerry 想再把 Mary 加入到對話中,需要如下代碼幫助他實現這個功能:
~~~
conversation.add(['Mary']).then(function(conversation) {
console.log('添加成功', conversation.members);
// 添加成功 ['Bob', 'Harry', 'William', 'Tom', 'Mary']
}).catch(console.error.bind(console));
~~~
##### 添加成員相關事件
添加成員后,對話中的成員會收到事件的通知,各方收到的事件是這樣的:
| 邀請者 | 被邀請者 | 其他人 |
| --- | --- | --- |
| `membersjoined` | `invited`?與?`membersjoined` | `membersjoined` |
> 注意:如果在進行邀請操作時,被邀請者不在線,那么通知消息并不會被離線緩存,所以等到 Ta 再次上線的時候將不會收到通知。
>
>
>
> +
>
>
#### [移除成員](#移除成員)
##### 自身退出對話
Tom 主動從對話中退出,他需要如下代碼實現需求:
~~~
conversation.quit().then(function(conversation) {
console.log('退出成功', conversation.members);
// 退出成功 ['Bob', 'Harry', 'William', 'Mary']
}).catch(console.error.bind(console));
~~~
##### 移除其他成員
Harry 被 William 從對話中刪除。實現代碼如下(關于 William 如何獲得權限在后面的?[安全與簽名](#安全與簽名)?中會做詳細闡述,此處不擴大話題范圍。):
~~~
realtime.createIMClient('William').then(function(william) {
return william.getConversation(CONVERSATION_ID);
}).then(function(conversation) {
return conversation.remove(['Harry']);
}).then(function(conversation) {
console.log('移除成功', conversation.members);
// 移除成功 ['Bob', 'William', 'Mary']
}).catch(console.error.bind(console));
~~~
##### 移除成員相關事件
移除成員后,對話中的成員會收到事件的通知,各方收到的事件是這樣的:
| 操作者 | 被移除者 | 其他人 |
| --- | --- | --- |
| `membersleft` | `kicked` | `membersleft` |
> 注意:如果在進行踢人操作時,被踢者不在線,那么通知消息并不會被離線緩存,所以等到 Ta 再次上線的時候將不會收到通知。
>
>
>
> +
>
>
#### [查詢成員數量](#查詢成員數量)
除了直接訪問?`conversation.members.length`,也可以通過?`Conversation#count`?方法獲得當前對話的成員數量:
~~~
conversation.count().then(function(membersCount) {
console.log(membersCount);
}).catch(console.error.bind(console));
~~~
### [對話的屬性管理](#對話的屬性管理)
對話實例(Conversation)與控制臺中?`_Conversation`?表是一一對應的,默認提供的屬性的對應關系如下:
1
| Conversation 屬性名 | _Conversation 字段 | 含義 |
| --- | --- | --- |
| `id` | `objectId` | 全局唯一的 Id |
| `name` | `name` | 成員共享的統一的名字 |
| `members` | `m` | 成員列表 |
| `creator` | `c` | 對話創建者 |
| `transient` | `tr` | 是否為聊天室(暫態對話) |
| `system` | `sys` | 是否為系統對話 |
| `mutedMembers` | `mu` | 靜音該對話的成員 |
| `muted` | N/A | 當前用戶是否靜音該對話 |
| `createdAt` | `createdAt` | 創建時間 |
| `updatedAt` | `updatedAt` | 最后更新時間 |
| `lastMessageAt` | `lm` | 最后一條消息發送時間,也可以理解為最后一次活躍時間 |
| `lastMessage` | N/A | 最后一條消息,可能會空 |
| `unreadMessagesCount` | N/A | 未讀消息數 |
#### [名稱](#名稱)
這是一個全員共享的屬性,它可以在創建時指定,也可以在日后的維護中被修改。
Tom 想建立一個名字叫「喵星人」 對話并且邀請了好友 Black 加入對話:
~~~
tom.createConversation({
members: ['Black'],
name: '喵星人',
}).then(function(conversation) {
console.log('創建成功。id: ' + conversation.id + ' name: ' + conversation.name);
}).catch(console.error.bind(console));
~~~
Black 發現對話名字不夠酷,他想修改成「聰明的喵星人」 ,他需要如下代碼:
~~~
black.getConversation(CONVERSATION_ID).then(function(conversation) {
conversation.name = '聰明的喵星人';
return conversation.save();
}).then(function(conversation) {
console.log('更新成功。name: ' + conversation.name);
}).catch(console.error.bind(console));
~~~
#### [成員](#成員)
是當前對話中所有成員的?`clientId`。默認情況下,創建者是在包含在成員列表中的,直到 TA 退出對話。
> 切勿在控制臺中對其進行修改。所有關于成員的操作請參照上一章節中的?[對話的成員管理](#對話的成員管理)?來進行。
>
>
>
> +
>
>
#### [靜音](#靜音)
假如某一用戶不想再收到某對話的消息提醒,但又不想直接退出對話,可以使用靜音操作,即開啟「免打擾模式」。
比如 Tom 工作繁忙,對某個對話設置了靜音:
~~~
black.getConversation(CONVERSATION_ID).then(function(conversation) {
return conversation.mute();
}).then(function(conversation) {
console.log('靜音成功');
}).catch(console.error.bind(console));
~~~
> 設置靜音之后,iOS 和 Windows Phone 的用戶就不會收到推送消息了。
>
>
>
> +
>
>
與之對應的就是取消靜音的操作,即取消免打擾模式。此操作會修改云端?`_Conversation`?里面的?`mu`?屬性。切勿在控制臺中對?`mu`?進行修改。
#### [創建者](#創建者)
即對話的創建者,它的值是對話創建者的?`clientId`。
它等價于 QQ 群中的「群創建者」,但區別于「群管理員」。比如 QQ 群的「創建者」是固定不變的,它的圖標顏色與「管理員」的圖標顏色都不一樣。所以根據對話中成員的?`clientId`?是否與?`conversation.creator`?一致就可以判斷出他是不是群的創建者。
#### [自定義屬性](#自定義屬性)
開發者可以為對話添加自定義屬性,來滿足業務邏輯需求。
給某個對話加上兩個自定義的屬性:type = "private"(類型為私有)、pinned = true(置頂顯示):
~~~
tom.createConversation({
members: ['Jerry'],
name: '貓和老鼠',
type: 'private',
pinned: true,
}).then(function(conversation) {
console.log('創建成功。id: ' + conversation.id);
}).catch(console.error.bind(console));
~~~
自定義屬性在 SDK 級別是對所有成員可見的。要對屬性進行查詢,請參見?[對話的查詢](#對話的查詢)。
### [對話的查詢](#對話的查詢)
#### [根據 id 查詢](#根據_id_查詢)
假如已知某一對話的 Id,可以使用它來查詢該對話的詳細信息:
~~~
tom.getConversation(CONVERSATION_ID).then(function(conversation) {
console.log(conversation.id);
}).catch(console.error.bind(console));
~~~
#### [對話列表](#對話列表)
用戶登錄進應用后,獲取最近的 10 個對話(包含暫態對話,如聊天室):
3
~~~
tom.getQuery().containsMembers(['Tom']).find().then(function(conversations) {
// 默認按每個對話的最后更新日期(收到最后一條消息的時間)倒序排列
conversations.map(function(conversation) {
console.log(conversation.lastMessageAt.toString(), conversation.members);
});
}).catch(console.error.bind(console));
~~~
對話的查詢默認返回 10 個結果,若要更改返回結果數量,請設置?`limit`?值。
~~~
var query = tom.getQuery();
query.limit(20).containsMembers(['Tom']).find().then(function(conversations) {
console.log(conversations.length);
}).catch(console.error.bind(console));
~~~
#### [條件查詢](#條件查詢)
~~~
// 查詢對話名稱為「LeanCloud 粉絲群」的對話
query.equalTo('name', 'LeanCloud 粉絲群');
// 查詢對話名稱包含 「LeanCloud」 的對話
query.contains('name', 'LeanCloud');
// 查詢過去24小時活躍的對話
var yesterday = new Date(Date.now() - 24 * 3600 * 1000);
query.greaterThan('lm', yesterday);
~~~
條件查詢又分為:比較查詢、正則匹配查詢、包含查詢,以下會做分類演示。
##### 比較查詢
比較查詢在一般的理解上都包含以下幾種:
比較查詢最常用的是等于查詢:
~~~
// topic 是自定義屬性
query.equalTo('topic','movie');
~~~
下面檢索一下類型不是私有的對話:
~~~
// type 是自定義屬性
query.notEqualTo('type','private');
~~~
對于可以比較大小的整型、浮點等常用類型,可以參照以下示例代碼進行擴展:
~~~
// age 是自定義屬性
query.greaterThan('age',18);
~~~
##### 正則匹配查詢
匹配查詢是指在?`ConversationQuery`?的查詢條件中使用正則表達式來匹配數據。
比如要查詢所有 language 是中文的對話:
~~~
// 自定義屬性 language 是中文字符
query.matches('language',/[\\u4e00-\\u9fa5]/);
~~~
##### 包含查詢
包含查詢是指方法名字包含?`Contains`?單詞的方法,例如查詢關鍵字包含「教育」的對話:
~~~
// 自定義屬性 keywords 包含「教育」
query.contains('keywords','教育');
~~~
另外,包含查詢還能檢索與成員相關的對話數據。以下代碼將幫助 Tom 查找出 Jerry 以及 Bob 都加入的對話:
~~~
// 查詢對話成員有 Bob 和 Jerry 的 conversations
query.withMembers(['Bob', 'Jerry']);
~~~
##### 組合查詢
組合查詢的概念就是把諸多查詢條件合并成一個查詢,再交給 SDK 去云端進行查詢。
例如,要查詢年齡小于 18 歲,并且關鍵字包含「教育」的對話:
~~~
// 查詢 keywords 包含「教育」且 age 小于 18 的對話
query.contains('keywords', '教育').lessThan('age', 18);
~~~
#### [查詢結果選項](#查詢結果選項)
##### 排序
`ConversationQuery`?支持使用?`ascending`、`addAscending`、`descending`、`addDescending`?方法來對查詢結果進行排序:
~~~
// 對查詢結果按照 name 升序,然后按照創建時間降序排序
query.addAscending('name').addDescending('createdAt');
~~~
##### 精簡模式
普通對話最多可以容納 500 個成員,在有些業務邏輯不需要對話的成員列表的情況下,可以使用?`ConversationQuery`?的?`compact`?方法指定查詢為「精簡模式」,返回的查詢結果中則不會有成員列表(`members`?字段會是空數組),這有助于提升應用的性能同時減少流量消耗。
~~~
query.compact(true);
~~~
##### 對話的最后一條消息
對于一個聊天應用,一個典型的需求是在對話的列表界面顯示最后一條消息,默認情況下,`ConversationQuery`?的查詢結果是不帶最后一條消息的,使用?`withLastMessagesRefreshed`?方法可以指定讓查詢結果帶上最后一條消息:
~~~
query.withLastMessagesRefreshed(true);
~~~
需要注意的是,這個選項真正的意義是「刷新對話的最后一條消息」。這意味著由于 SDK 緩存機制的存在,將這個選項設置為?`false`?查詢得到的對話也還是有可能會存在最后一條消息的。
#### [緩存查詢](#緩存查詢)
JavaScript SDK 會對按照對話 id 對對話進行內存字典緩存,但不會進行持久化的緩存。
## [聊天室](#聊天室)
聊天室本質上就是一個對話,所以上面章節提到的所有屬性、方法、操作以及管理都適用于聊天室。它僅僅在邏輯上是一種暫態、臨時的對話,應用場景有彈幕、直播等等。
聊天室與普通對話或群聊不一樣的地方具體體現為:
* 無人數限制,而普通對話最多允許 500 人加入。
* 不支持查詢成員列表,但可以通過相關 API 查詢在線人數。
* 不支持離線消息、離線推送通知、消息回執等功能。
* 沒有成員加入、成員離開的通知。
* 一個用戶一次登錄只能加入一個聊天室,加入新的聊天室后會自動離開原來的聊天室。
* 加入后半小時內斷網重連會自動加入原聊天室,超過這個時間則需要重新加入。
### [創建聊天室](#創建聊天室)
建立一個聊天室需要在?`IMClient#createConversation()`?時傳入?`transient=true`。
比如喵星球正在直播選美比賽,主持人 Tom 創建了一個臨時對話,與喵粉們進行互動:
~~~
tom.createConversation({
name: 'Hello Kitty PK 加菲貓',
transient: true,
}).then(function(conversation) {
console.log('創建聊天室成功。id: ' + conversation.id);
}).catch(console.error.bind(console));
~~~
### [查詢在線人數](#查詢在線人數)
`Conversation.count()`?可以用來查詢普通對話的成員總數,在聊天室中,它返回的就是實時在線的人數:
~~~
conversation.count().then(function(count) {
console.log('在線人數: ' + count);
}).catch(console.error.bind(console));
~~~
### [查找聊天室](#查找聊天室)
開發者需要注意的是,通過?`IMClient#getQuery()`?這樣得到的?`ConversationQuery`?實例默認是查詢全部對話的,也就是說,如果想查詢指定的聊天室,需要限定?`tr`?字段的查詢條件:
比如查詢主題包含「奔跑吧,兄弟」的聊天室:
~~~
var query = tom.getQuery();
query
.equalTo('topic', '奔跑吧,兄弟')
.equalTo('tr', true)
.find()
.then(function(conversations) {
console.log(conversations[0].id);
})
.catch(console.error.bind(console));
~~~
## [聊天記錄](#聊天記錄)
聊天記錄一直是客戶端開發的一個重點,QQ 和 微信的解決方案都是依托客戶端做緩存,當收到一條消息時就按照自己的業務邏輯存儲在客戶端的文件或者是各種客戶端數據庫中。
我們的 SDK 會將普通的對話消息自動保存在云端,開發者可以通過?`Conversation#queryMessages`?方法來獲取該對話的所有歷史消息。
獲取該對話中最近的 N 條(默認 20,最大值 1000)歷史消息,通常在第一次進入對話時使用:
~~~
conversation.queryMessages({
limit: 10, // limit 取值范圍 1~1000,默認 20
}).then(function(messages) {
// 最新的十條消息,按時間增序排列
}).catch(console.error.bind(console));
~~~
對于翻頁加載更多歷史消息的場景,SDK 還提供了?`Conversation#createMessagesIterator`?方法來生成一個歷史消息迭代器。假如每一頁為 10 條信息,下面的代碼將演示如何翻頁:
~~~
// 創建一個迭代器,每次獲取 10 條歷史消息
var messageIterator = conversation.createMessagesIterator({ limit: 10 });
// 第一次調用 next 方法,獲得前 10 條消息,還有更多消息,done 為 false
messageIterator.next().then(function(result) {
// result: {
// value: [message1, ..., message10],
// done: false,
// }
}).catch(console.error.bind(console));
// 第二次調用 next 方法,獲得第 11 ~ 20 條消息,還有更多消息,done 為 false
messageIterator.next().then(function(result) {
// result: {
// value: [message11, ..., message20],
// done: false,
// }
}).catch(console.error.bind(console));
// 第二次調用 next 方法,獲得第 21 條消息,沒有更多消息,done 為 true
messageIterator.next().then(function(result) {
// No more messages
// result: { value: [message21], done: true }
}).catch(console.error.bind(console));
~~~
### [客戶端聊天記錄緩存](#客戶端聊天記錄緩存)
JavaScript SDK 沒有客戶端聊天記錄緩存機制
## [客戶端事件](#客戶端事件)
### [網絡狀態響應](#網絡狀態響應)
> 注意:在網絡中斷的情況下,所有的消息收發和對話操作都會失敗。開發者應該監聽與網絡狀態相關的事件并更新 UI,以免影響用戶的使用體驗。
>
>
>
> +
>
>
當網絡連接出現中斷、恢復等狀態變化時,SDK 會在 Realtime 實例上派發以下事件:
* `disconnect`:網絡連接斷開,此時聊天服務不可用。
* `schedule`:計劃在一段時間后嘗試重連,此時聊天服務仍不可用。
* `retry`:正在重連。
* `reconnect`:網絡連接恢復,此時聊天服務可用。
~~~
realtime.on('disconnect', function() {
console.log('網絡連接已斷開');
});
realtime.on('schedule', function(attempt, delay) {
console.log(delay + 'ms 后進行第' + (attempt + 1) + '次重連');
});
realtime.on('retry', function(attempt) {
console.log('正在進行第' + attempt + '次重連');
});
realtime.on('reconnect', function() {
console.log('網絡連接已恢復');
});
~~~
在?`schedule`?與?`retry`?事件之間,開發者可以調用?`Realtime#retry()`?方法手動進行重連。
在斷線重連的過程中,SDK 也會在所有的 IMClient 實例上派發同名的事件。Realtime 與 IMClient 上的同名事件是先后同步派發的,唯一的例外是?`reconnect`?事件。在網絡連接恢復,Realtime 上派發了?`reconnect`?事件之后,IMClient 會嘗試重新登錄,成功后再派發?`reconnect`?事件。所以,Realtime 的?`reconnect`?事件意味著 Realtime 實例的 API 能夠正常使用了,IMClient 的?`reconnect`?事件意味著 IMClient 實例的 API 能夠正常使用了。
下面顯示的是一次典型的斷線重連過程中 SDK 派發的事件:
~~~
// 連接斷開,計劃 1s 后重連
[Realtime & IMClient] disconnect
[Realtime & IMClient] schedule (attempt=0, delay=1000)
// 1s 后,嘗試重連
[Realtime & IMClient] retry (attempt=0)
// 重連失敗,計劃 2s 后進行第二次重連
[Realtime & IMClient] schedule (attempt=1, delay=2000)
// 在 2s 內,手動調用 realtime.retry() 進行重連,重連次數重置
[Realtime & IMClient] retry (attempt=0)
// 重連失敗,計劃 2s 后進行第二次重連
[Realtime & IMClient] schedule (attempt=1, delay=2000)
// 2s 后,嘗試第二次重連
[Realtime & IMClient] retry (attempt=1)
// 連接恢復,此時可以創建新的客戶端了
[Realtime] reconnect
// 客戶端重新登錄上線,此時該客戶端可以收發消息了
[IMClient] reconnect
~~~
## [退出登錄](#退出登錄)
tom 要退出當前的登錄狀態或要切換賬戶,方法如下:
~~~
tom.close().then(function() {
console.log('Tom 退出登錄');
}).catch(console.error.bind(console));
~~~
## [安全與簽名](#安全與簽名)
在繼續閱讀下文之前,請確保你已經對?[實時通信服務開發指南 · 權限和認證](https://leancloud.cn/docs/realtime_v2.html#權限和認證)?有了充分的了解。
### [實現簽名工廠](#實現簽名工廠)
為了滿足開發者對權限和認證的要求,我們設計了操作簽名的機制。簽名啟用后,所有的用戶登錄、對話創建/加入、邀請成員、踢出成員等登錄都需要驗證簽名,這樣開發者就對消息具有了完全的掌控。
我們強烈推薦啟用簽名,具體步驟是?[控制臺 > 設置 > 應用選項](https://leancloud.cn/app.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/permission),勾選?聊天、推送?下的?聊天服務,啟用簽名認證。
客戶端這邊究竟該如何使用呢?我們只需要實現 signature 工廠方法,然后作為參數實例化 IMClient 即可
設定了 signature 工廠方法后,對于需要鑒權的操作,實時通信 SDK 與服務器端通訊的時候都會帶上應用自己生成的 Signature 信息,LeanCloud 云端會使用 app 的 masterKey 來驗證信息的有效性,保證聊天渠道的安全。
對于不同的操作,我們需要實現兩個不同的 signature 工廠方法:`signatureFactory`?與?`conversationSignatureFactory`。
~~~
/**
* IMClient 登錄簽名工廠
*
* @param {String} clientId 登錄用戶 ID
* @return {Object} signatureResult
* @return {String} signatureResult.signature
* @return {Number} signatureResult.timestamp
* @return {String} signatureResult.nonce
*/
var signatureFactory = function(clientId) {
// to be implemented
};
/**
* Conversation 相關操作簽名工廠
*
* @param {String} conversationId
* @param {String} clientId 當前用戶 ID
* @param {String[]} targetIds 此次操作的目標用戶 IDs
* @param {String} action 此次行為的動作,可能的值為 create(創建會話)、add(加群和邀請)和 remove(踢出群)之一
* @return {Object} signatureResult
* @return {String} signatureResult.signature
* @return {Number} signatureResult.timestamp
* @return {String} signatureResult.nonce
*/
var conversationSignatureFactory = function(clientId) {
// to be implemented
};
~~~
`signatureFactory`?函數會在用戶登錄的時候被調用,`conversationSignatureFactory`?會在對話創建/加入、邀請成員、踢出成員等操作時被調用。
你需要做的就是按照前文所述的簽名算法實現簽名,返回 signatureResult,其中四個屬性分別是:
* signature 簽名
* timestamp 時間戳,單位秒
* nonce 隨機字符串 nonce
如果簽名是異步的,比如需要發送一個網絡請求,那么 signature 工廠方法也可以返回一個 Promise,resolved with signatureResult。
下面的代碼展示了基于 LeanCloud 云引擎進行簽名時,客戶端的實現片段,你可以參考它來完成自己的邏輯實現:
~~~
var signatureFactory = function(clientId) {
return AV.Cloud.rpc('sign', { clientId: clientId }); // AV.Cloud.rpc returns a Promise
};
var conversationSignatureFactory = function(conversationId, clientId, targetIds, action) {
return AV.Cloud.rpc('sign-conversation', {
conversationId: conversationId,
clientId: clientId,
targetIds: targetIds,
action: action,
});
};
realtime.createIMClient('Tom', {
signatureFactory: signatureFactory,
conversationSignatureFactory: conversationSignatureFactory,
}).then(function(tom) {
console.log('Tom 登錄');
}).catch(function(error) {
// 如果 signatureFactory 拋出了異常,或者簽名沒有驗證通過,會在這里被捕獲
});
~~~
> 需要強調的是:開發者切勿在客戶端直接使用 MasterKey 進行簽名操作,因為 MaterKey 一旦泄露,會造成應用的數據處于高危狀態,后果不容小視。因此,強烈建議開發者將簽名的具體代碼托管在安全性高穩定性好的服務器上(例如 LeanCloud 云引擎)。
>
>
>
> +
>
>
### [單點登錄](#單點登錄)
一款聊天應用,隨著不斷的發展,會衍生出多個平臺的不同客戶端。以 QQ 為例,目前它所提供的客戶端如下:
* PC:Windows PC、Mac OS、Linux(已停止更新)
* 移動:Windows Phone、iOS、Android
* Web:[http://w.qq.com/](http://w.qq.com/)
經過測試,我們發現 QQ 存在以下幾種行為:
1. 同一個 QQ 賬號不可以同時在 2 個 PC 端登錄(例如,在 Mac OS 上登錄已經在另外一臺 Windows PC 上登錄的 QQ,該 QQ 號在 Windows PC 上會被強行下線)。
2. 同一個 QQ 賬號不可以同時在 2 個移動端上登錄。
3. Web QQ 也不能與 PC 端同時登錄
4. 同一個 QQ 只能同時在 1 個移動版本和 1 PC 版本(或者 Web 版本)上登錄,并實現一些 PC 與移動端互動的功能,例如互傳文件。
通過規律不難發現,QQ 按照自己的需求實現了「單點登錄」的功能:同一個平臺上只允許一個 QQ 登錄一臺設備。
下面我們來詳細說明:如何使用我們的 SDK 去實現單點登錄。
#### [設置登錄標記 Tag](#設置登錄標記_Tag)
假設開發者想實現 QQ 這樣的功能,那么需要在登錄到服務器的時候,也就是打開與服務器長連接的時候,標記一下這個鏈接是從什么類型的客戶端登錄到服務器的:
~~~
realtime.createIMClient('Tom', null, 'Web').then(function(tom) {
console.log('Tom 登錄');
});
~~~
上述代碼可以理解為 LeanCloud 版 QQ 的登錄,而另一個帶有同樣 Tag 的客戶端打開連接,則較早前登錄系統的客戶端會被強制下線。
#### [處理登錄沖突](#處理登錄沖突)
我們可以看到上述代碼中,登錄的 Tag 是?`Web`。當存在與其相同的 Tag 登錄的客戶端,較早前登錄的設備會被服務端強行下線,而且他會收到被服務端下線的通知:
~~~
tom.on('conflict', function() {
// 彈出提示,告知當前用戶的 Client Id 在其他設備上登陸了
});
~~~
如上述代碼中,當前用戶被服務端強行下線時,SDK 會在 client 上派發?`conflict`?事件,客戶端在做展現的時候也可以做出類似于 QQ 一樣友好的通知。
## [插件](#插件)
SDK 支持通過插件來對功能進行擴展,比如在解析消息前對原始消息進行修改,為內部的類添加方法,注冊自定義消息等。
### [插件列表](#插件列表)
請參閱?[https://github.com/leancloud/js-realtime-sdk/wiki/Plugins]()。
### [使用插件](#使用插件)
Realtime 支持在初始化時傳入指定一個 plugins 數組:
~~~
var Realtime = require('leancloud-realtime').Realtime;
var WebRTCPlugin = require('leancloud-realtime-plugin-webrtc').WebRTCPlugin;
var realtime = new Realtime({
appId: appId,
plugins: [WebRTCPlugin],
});
~~~
插件的具體使用方式請參考具體插件的文檔。
### [創建插件](#創建插件)
#### [擴展點](#擴展點)
一個插件是由一個或多個擴展點組成的字典(Object)。擴展點可以分為三類:
第一類擴展點是 SDK 內部類實例化之后的回調,包括?`onRealtimeCreated`、`onIMClientCreated`?與?`onConversationCreated`。這些擴展點可以通過一個方法(function)進行擴展,該方法接受一個對應的實例并對其進行一些操作。我們稱這一類方法為 Decorator。插件可以利用這些擴展點為內部類添加新的方法或修改原有的方法。
下面這個例子利用了?`onConversationCreate`?擴展點,修改了 Conversation 的 quit 方法,在調用 quit 方法時統一彈出一個確認窗口。
~~~
var ConfirmOnQuitPlugin = {
name: 'leancloud-realtime-plugin-confirm-on-quit',
onConversationCreate: function onConversationCreate(conversation) {
var originalQuit = conversation.quit;
conversation.quit = function() {
var confirmed = window.confirm('退出會話?退出后將無法收到消息。');
if (confirmed) {
return originalQuit.apply(this, arguments);
} else {
return Promise.reject(new Error('user canceled'));
}
}
}
};
~~~
第二類擴展點允許你在某些事件前、后注入邏輯。這些擴展點可以通過一個方法(function)進行擴展,該方法接受一個對象,返回一個同類型對象(如果該方法是異步的,則返回一個 Promise)。我們稱這一類方法為 Middleware。 以消息解析為例,可以將 SDK 從接收原始消息 - 解析消息 - 派發的富媒體消息的過程看成一條管道,這些擴展點允許你在這個管道中加入一段你的節點,這個節點就是 Middleware。如果指定了多個 Middleware,這些 Middleware 會按照順序依次執行,前一個 Middleware 的返回值會作為參數傳給后一個 Middleware。
目前可擴展的點有:
* `beforeMessageParse`: 在解析消息前對原始消息進行處理,參數是 json 格式的原始消息
* `afterMessageParse`: 在解析消息后對消息進行處理,參數是對應的富媒體消息類的實例
舉個例子,有一些對話中存在一些 FileMessage 類型的歷史消息,由于某種原因缺少了必須的 file.id 字段,會導致解析到這些消息時 SDK 拋出異常。這時可以通過?`beforeMessageParse`?擴展點來在 SDK 解析消息前「修補」這個問題。
~~~
var EnsureFileIdPlugin = {
name: 'leancloud-realtime-plugin-ensure-file-id',
beforeMessageParse: function onConversationCreate(message) {
if (!message._lcfile.id) message._lcfile.id = '';
return message;
}
};
~~~
第三類擴展點是一個特殊的擴展點:`messageClasses`,這是一個由自定義消息類型組成的數組,數組中的自定義消息類型會被自動注冊(通過 Realtime#register)。在富文本消息一節中用到的 TypedMessagesPlugin 就是使用了這個擴展點的插件。
如果有必要,我們會在未來開放更多的擴展點。
#### [插件規范](#插件規范)
如果你的插件可能會被其他開發者用到,我們推薦你將其封裝為一個 package 并發布到 npm 上,發布的插件請遵循以下規范:
* package 名稱以?`leancloud-realtime-plugin-`?為前綴;
* 插件對象需要有?`name`?字段,用于在日志中顯示異常的插件名稱,建議與 package 名稱相同。
## [從 v2 遷移](#從_v2_遷移)
如果你的應用正在使用 JavaScript SDK version 2 并希望升級到 version 3,請參考?[《JavaScript 實時通信 SDK v3 遷移指南》](https://leancloud.cn/docs/realtime_js-v3-migration-guide.html)。
## [常見問題](#常見問題)
我只想實現兩個用戶的私聊,是不是每次都得重復創建對話?
答:不需要重復創建。我們推薦的方式是開發者可以用自定義屬性來實現對私聊和群聊的標識,并且在進行私聊之前,需要查詢當前兩個參與對話的 ClientId 是否之前已經存在一個私聊的對話了。另外,SDK 已經提供了創建唯一對話的接口,請查看?[創建對話](#創建對話)。
某個成員退出對話之后,再加入,在他離開的這段期間內的產生的聊天記錄,他還能獲取么?
1
答:可以。目前聊天記錄從屬關系是屬于對話的,也就是說,只要對話 Id 不變,不論人員如何變動,只要這個對話產生的聊天記錄,當前成員都可以獲取。
我自己沒有服務器,如何實現簽名的功能?
答:LeanCloud 云引擎提供了托管 Python 和 Node.js 運行的方式,開發者可以所以用這兩種語言按照簽名的算法實現簽名,完全可以支持開發者的自定義權限控制。
## [問題排查](#問題排查)
1. 客戶端連接被關閉有許多原因,請參考?[服務器端錯誤碼說明](https://leancloud.cn/docs/realtime_v2.html#服務器端錯誤碼說明)。