# [mui初級入門教程(五)— 聊聊即時通訊(IM),基于環信 web im SDK](https://www.cnblogs.com/PheonixHkbxoic/p/6013364.html)
## 寫在前面
感覺自從`qq`、微信這種`APP`用多了,現在都沒啥人發短信了,現在什么`APP`都想加入`IM`的功能,曾經有段時間在折騰自己擼一個聊天的東西,也嘗試過很多平臺,今天這里給大家介紹一下從零開始自己做一個聊天的`app`功能。因為之前幫朋友做過一個基于環信的聊天功能,這里就以環信的平臺為例舉個例子說明。這篇文章注意想講解一下集成這種第三方的一般實現方法,不會一下子就把所有的功能都集成,因為之前做環信主要是在微信上用,所以用的是環信的`Web IM`,遇到了蠻多坑,這次打算用`dcloud`這邊的`mui`重新集成,所以在沒有完全做完之前,所以也不知道有些坑具體能夠在有限的時間內解決,本文僅供參考,歡迎大家去實踐檢驗。在寫這篇文章之前先貼一個`Dcloud`論壇中的資源帖,[【即時通信、im問題匯總】](http://ask.dcloud.net.cn/article/649)
## 準備工作
### 1.注冊賬號
我們要先去[環信官網](http://www.easemob.com/product/im)注冊一個賬號,然后在后臺創建一個應用,因為我們后面在做功能的時候可以用后面發送消息及圖片來測試收消息,用戶管理在后臺也可以看得一清二楚。

創建成功后找到應用標識(AppKey),這個在后期配置中會用到。
### 2.下載SDK
[http://www.easemob.com/download/im](http://www.easemob.com/download/im)
這里我們使用的是`Web IM`,所以下載的`SDK`是`Web IM`版本,下載之后我們會看到一個演示`demo`,由于這個是`pc`版本,和我們需求不一致,所以我們只需要關心`sdk`目錄下的文件和sdk集成需要修改的配置文件`easemob.im.config.js`。
~~~
|---README.MD:
|---index.html:demo首頁,包含sdk基礎功能和瀏覽器兼容性的解決方案
|---static/:
js/:
easemob.im.config.js:sdk集成需要修改的配置文件
css/:
img/:
sdk/:/*sdk相關文件*/
release.txt:各版本更新細節
quickstart.md:環信WebIM快速入門文檔
easemob.im-1.1.js:js sdk
easemob.im-1.1.shim.js:支持老版本sdk api
strophe.js:sdk依賴腳本
~~~
### 3.開發文檔
Web IM 介紹[http://docs.easemob.com/im/400webimintegration/10webimintro](http://docs.easemob.com/im/400webimintegration/10webimintro)
## 項目實戰
由于這篇重在在于如何使用第三方開發`IM`,感覺說再多也誒有意義,直接上代碼說明。不講解過多的原理、細節,只講究開發流程。
### 1.用戶注冊功能
首先我們在hbuilder中先新建一個項目`easemobIM`,然后把環信`sdk`文件夾和配置文件拷貝到我們的工程中。為了節約時間,下面的功能演示我是根據官方登錄模板改的。
**html/reg.html**
~~~
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
<link href="../css/mui.min.css" rel="stylesheet" />
<link href="../css/style.css" rel="stylesheet" />
<style>
.mui-input-group:first-child {
margin-top: 20px;
}
.mui-input-group label {
width: 22%;
}
.mui-input-row label~input,
.mui-input-row label~select,
.mui-input-row label~textarea {
width: 78%;
}
.mui-checkbox input[type=checkbox],
.mui-radio input[type=radio] {
top: 6px;
}
.mui-content-padded {
margin-top: 25px;
}
.mui-btn {
padding: 10px;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">注冊</h1>
</header>
<div class="mui-content">
<form class="mui-input-group">
<div class="mui-input-row">
<label>手機</label>
<input id='username' type="text" class="mui-input-clear mui-input" placeholder="請輸入手機號碼">
</div>
<div class="mui-input-row">
<label>昵稱</label>
<input id='nickname' type="text" class="mui-input-clear mui-input" placeholder="請輸入昵稱">
</div>
<div class="mui-input-row">
<label>密碼</label>
<input id='password' type="password" class="mui-input-clear mui-input" placeholder="請輸入密碼">
</div>
<div class="mui-input-row">
<label>確認</label>
<input id='password_confirm' type="password" class="mui-input-clear mui-input" placeholder="請確認密碼">
</div>
</form>
<div class="mui-content-padded">
<button id='reg' class="mui-btn mui-btn-block mui-btn-primary">注冊</button>
</div>
</div>
<script src="../js/mui.min.js"></script>
<!--sdk-->
<script src="../sdk/strophe.js"></script>
<script src="../sdk/easemob.im-1.1.js"></script>
<script src="../sdk/easemob.im-1.1.shim.js"></script><!--兼容老版本sdk需引入此文件-->
<!--config-->
<script src="../js/easemob.im.config.js"></script>
<script>
mui.init();
// 輸入參數
var regConfig = {
username: mui("#username")[0],
nickname: mui("#nickname")[0],
password: mui("#password")[0],
passwordConfirm: mui("#password_confirm")[0]
};
// 注冊事件監聽
mui("#reg")[0].addEventListener('tap',function(){
var username = regConfig.username.value;
var nickname = regConfig.nickname.value;
var password = regConfig.password.value;
var passwordConfirm = regConfig.passwordConfirm.value;
// 電話號碼校驗
if (!isMobile(username)){
mui.toast("電話號碼格式不正確");
return;
}
// 昵稱非空校驗
if (!isEmpty(nickname)){
mui.toast('昵稱不能為空');
return;
}
// 密碼非空校驗
if (!isEmpty(password)){
mui.toast('密碼不能為空');
return;
}
// 密碼重復校驗
if (passwordConfirm != password) {
mui.toast('密碼兩次輸入不一致');
return;
}
// 環信SDK注冊
var options = {
username : username,
password : password,
nickname : nickname,
appKey : Easemob.im.config.appkey,
success : function(result) {
//注冊成功;
console.log(JSON.stringify(result))
mui.toast('注冊成功');
},
error : function(e) {
//注冊失敗;
console.log(JSON.stringify(e));
mui.toast('注冊失敗:'+e.error);
}
};
Easemob.im.Helper.registerUser(options);
});
// 是否為電話號碼
function isMobile(value) {
var validateReg = /0?(13|14|15|18)[0-9]{9}/;
return validateReg.test(value);
}
// 是否為空
function isEmpty(value){
var validateReg = /^\S+$/;
return validateReg.test(value);
}
</script>
</body>
</html>
~~~
這是注冊頁面的代碼,我們首先要引入環信的`sdk`和`easemob.im.config.js`,并且將`easemob.im.config.js`中的`appkey`換成自己的,然后根據用戶名/密碼/昵稱注冊環信 Web IM,提交注冊的代碼為:
~~~
var options = {
username : username,
password : password,
nickname : nickname,
appKey : Easemob.im.config.appkey,
success : function(result) {
//注冊成功;
console.log(JSON.stringify(result))
mui.toast('注冊成功');
},
error : function(e) {
//注冊失敗;
console.log(JSON.stringify(e));
mui.toast('注冊失敗:'+e.error);
}
};
Easemob.im.Helper.registerUser(options);
~~~
我們注冊完了后可以在環信后臺【IM用戶】查看用戶注冊信息,我們我們用其他平臺,只需要把這塊的內容改成相應的內容就OK。
### 2.用戶登錄功能
有了注冊頁面的經驗,我們寫登錄頁面也很簡單,頁面布局腳本和其他與登錄邏輯無關的代碼我這里不貼了,大家在我最后給的地址上下載完整代碼,這里只講解基本基本思路。環信登錄優兩種方法,一種是通過實例化`new Easemob.im.Connection()`建立連接,一種是使用工具類`Easemob.im.Helper.login2UserGrid(options)`,我們剛剛注冊就是使用了工具類,為了便于大家后面的學習,我們在這里把兩種方法都說一下:
#### 實例化`new Easemob.im.Connection()`建立連接
**1.創建連接**
~~~
var conn =?new Easemob.im.Connection();
~~~
**2.初始化連接**
~~~
conn.init({
onOpened : function() {
alert("成功登錄");
conn.setPresence();
}
});
~~~
**3.初始化連接**
~~~
// 打開連接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
~~~
> 這里我們需要注意的是`open()`方法中需要配置的屬性是`user`和`pwd`,這和我們注冊時的有區別,要注意哦!
這里需要說明的是`init()`是環信提供的一個通用的方法,比如后面我們要用到的接收文本消息、圖片消息等一系列的回調方法都寫在這個里面,`onOpened()`方法主要是用于當執行`conn.open()`方法時需要執行的方法,我們一般會把頁面需要初始化的邏輯寫在`onOpened()`中,比如查詢好友。
**完整代碼:**
~~~
// 輸入參數
var loginConfig = {
username: mui("#username")[0],
password: mui("#password")[0]
};
// 創建一個新的連接
var conn = new Easemob.im.Connection();
// 初始化連接
conn.init({
onOpened : function() {
mui.toast("成功登錄");
conn.setPresence();
mui.openWindow({
url: 'html/tab-webview-main.html',
extras:{
username:loginConfig.username.value,
password:loginConfig.password.value
}
})
}
});
// 登錄事件監聽
mui("#login")[0].addEventListener('tap',function(){
var username = loginConfig.username.value;
var password = loginConfig.password.value;
// 電話號碼校驗
if (!isMobile(username)){
mui.toast("電話號碼格式不正確");
return;
}
// 密碼非空校驗
if (!isEmpty(password)){
mui.toast('密碼不能為空');
return;
}
// 打開連接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
});
~~~
#### 工具類`Easemob.im.Helper.login2UserGrid(options)`建立連接
~~~
// 登錄
var options = {
user : username,
pwd : password,
appKey : Easemob.im.config.appkey,
success:function(data){
console.log(JSON.stringify(data))
mui.toast("成功登錄");
mui.openWindow({
url: 'html/tab-webview-main.html',
extras:{
username:loginConfig.username.value,
password:loginConfig.password.value
}
})
},
error: function(e){
console.log(JSON.stringify(e))
mui.toast("成功失敗:"+e);
}
};
Easemob.im.Helper.login2UserGrid(options);
~~~
上面我們用了兩種方法講解了登錄的方法,各有優劣,第二種只做登錄的工作,代碼也比較簡潔,但是當我們的頁面是多個頁面時我們的登錄狀態是不能檢測到的,這個時候我們還是需要在每個頁面通過創建連接初始化,所以我們在頁面跳轉過程加入了拓展參數`extras`傳遞參數,然后在登陸后的頁面接收就可以。
### 3.頁面傳參深入探究
為了盡可能簡單的演示我們的功能,我這里不使用個性化的設計,就用官方模板組中的【mui底部選項卡(webview模式)】進行展示。新建模板文件如下:

我們去掉第一個選項卡,只保留消息`tab-webview-subpage-chat.html`、通訊錄`tab-webview-subpage-contact.html`、設置`tab-webview-subpage-setting.html`三個選項卡。
#### 拓展參數`extras`傳值
上一小節中,我們在登陸頁面通過拓展參數`extras`傳值,在主頁面接收數據的方法為:
~~~
mui.plusReady(function(){
var self = plus.webview.currentWebview();
var username = self.username;
var password = self.password;
mui.toast("username:"+username+"<br />"+"password:"+password);
});
~~~
在主界面`mui.plusReady`方法里面拿到值,然后可以在創建子`webview`時候用拓展參數傳值,然后在子頁用下面的方法用同樣的方法可以拿到值。但是其實我們不需要父頁面向子頁面發消息,直接在子頁面通過這個找到父頁面對象就OK了,如下:
**子頁面代碼:**
~~~
mui.plusReady(function(){
var self = plus.webview.currentWebview().parent();
var username = self.username;
var password = self.password;
console.log("username:"+username+"password:"+password);
});
~~~
#### 預加載時使用`mui.fire()`傳值
這里需要特別說明一下的是我們有時候想要預加載我們的主頁面,這里我們有個地方我需要特別注意的是,我們需要用`mui.fire()`傳遞參數:
> mui.fire(target,event,data)
特別提醒一下:`target`是需要接受參數的`webview`對象,而不是`id`,在這個地方我出過錯誤,當時一直沒有察覺,如果是`id`,需要使用`plus.webview.getWebviewById(id)`進行轉換。
比如我們在登陸頁面使用`preload`預加載,代碼如下:
~~~
...
var mainPage = null;
mui.plusReady(function(){
mainPage = mui.preload({
"url": 'html/tab-webview-main.html',
"id": 'main'
});
})
...
~~~
**登陸按鈕監聽事件中的success方法:**
~~~
mui.fire(mainPage,'show',{
username:loginConfig.username.value,
password:loginConfig.password.value
});
setTimeout(function() {
mui.openWindow({
id: 'main',
show: {
aniShow: 'pop-in'
},
waiting: {
autoShow: false
}
});
}, 0);
~~~
在主頁面中通過自定義`show`事件獲得參數:
~~~
var username=null,password=null;
// 頁面傳參數事件監聽
window.addEventListener('show',function(event){
// 獲得事件參數
username = event.detail.username;
password = event.detail.password;
console.log("username:"+username+"password:"+password);
});
~~~
我們需要注意的是我們剛剛在登錄頁面的賬號密碼傳遞到了`tab-webview-main.html`主頁面,但是我們的每個子頁面沒有拿到賬號密碼。這里就有個容易犯錯的地方,我們可能會直接在創建子`webview`時候通過拓展參數`extras`傳值。
> 經過試驗發現經過預加載的主界面`tab-webview-main.html`的`mui.plusReady`方法比頁面的自定義事件監聽先執行,這是因為我們通過預加載的時候其實已經就執行了`mui.plusReady`方法,而自定義事件是在`webview`打開的時候執行。當主界面被預加載時,子頁面的`loaded`事件也隨著完成,創建子頁面的時候我們根本就沒有拿到數據怎么傳,自然在子頁得到的是`undefined`。我們這個時候如果想在主界面生成子頁面的時候通過拓展參數`extras`傳遞給子頁面根本行不通!
>
> 當需要接受參數的`webview`已經完成`loaded`事件,我們就不能使用拓展參數`extras`傳參數,這個時候我們可以使用`webview.evalJS()`或者`mui.fire()`;另外我們使用`webview.evalJS()`或者`mui.fire()`時,接收參數的頁面的`loaded`事件也必須發生才能使用。
>
> mui傳參數只能相互關聯的兩個webview之間傳,比如A頁面打開B頁面,B頁面打開C頁面,A頁面可以傳值給B頁面,但是A頁面不能傳值給C頁面,我們可以通過B頁面傳給C頁面。
**驗證一個webview的loaded事件是否完成的方法:**
~~~
var ws = plus.webview.getWebviewById(id)
ws.addEventListener( "loaded", function(e){
console.log( "Loaded: "+e.target.getURL() );
}, false );
~~~
**驗證一個webview的show事件是否完成的方法:**
~~~
var ws=plus.webview.currentWebview();
ws.addEventListener("show", function(e){
console.log( "Webview Showed" );
}, false );
~~~
說這兩個監聽事件有啥用處呢,我們在預加載`webview`的時候,預加載完成的過程,`loaded`事件也隨之完成,但是只有頁面被打開時,`show`事件才完成,我們可以選擇合適的時機發送或者接受參數。
這里需要說明的是如果你想`localstorage`、`Storage`等本地存儲傳值,完全可以不用`extras`或者`mui.fire()`,當然還可以用`url`傳參數。
因為當初就是為了一個想法,預加載試試,然后試著試著各種問題,不過也因此明白了很多規則和調試方法,在這里提出來順便總結一下頁面傳參需要注意的問題,免得新手在此花了很多冤枉時間,搞得現在都快忘了前面寫了啥。其實這一部分可以獨立出來,但是總感覺這種東西不是啥難事,脫離實際去講總覺得不合適。
### 4.獲取好友列表及添加好友
#### 獲取好友列表
我們在登陸頁面與環信的服務器建立了聯系,但是由于我們執行跳轉了,我們依然還需要在需要請求數據時候在當前頁面再次建立連接,前面我們講到可以通過實例化`new Easemob.im.Connection()`建立連接,我們這里可以在當前頁面實例化建立連接,而不是使用登錄時的登陸工具類。實例化`new Easemob.im.Connection()`的三個步驟大家可以查看前面的內容,這里需要說明的是我們獲取好友列表是在`conn.init`方法的`onOpened : function(){};`中添加`getRoster`回調方法,從而獲取好友列表。
~~~
// 創建連接
var conn = new Easemob.im.Connection();
// 初始化連接
conn.init({
onOpened : function(){
// mui.toast("成功登錄");
conn.setPresence(); //設置在線狀態
conn.getRoster({
success : function(roster) {
console.log(JSON.stringify(roster))
// 獲取當前登錄人的好友列表
for ( var i in roster) {
var ros = roster[i]; //好友的對象
//ros.name為好友名稱
}
}
});
}
});
mui.plusReady(function(){
var self = plus.webview.currentWebview().parent();
var username = self.username;
var password = self.password;
console.log("username:"+username+"password:"+password);
// 打開連接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
});
~~~
很顯然我們在執行后是空的,因為從開始到現在我們都是自己和自己玩,都沒有找朋友,那下面我們就去找朋友,之所以先要把這個先寫出來,因為這個我覺得是基本邏輯,你待會兒加了好友,怎么看,就通過這里查詢,然后才能說后面的聊天。
#### 添加好友
首先我們得去邀請對方吧,那么我們得知道對方的號碼吧,上面我們用的是手機號碼作為用戶名,為的就是保證用戶`ID`唯一性。
**邀請發起方:**
我們通過執行`conn.subscribe`可以發起邀請,添加發起方,獲取要添加好友名稱,參數為:
~~~
{
to: user, //對方用戶名
message:"加個好友唄" //對方收到的消息
}
~~~
這里我們在頭部右上角叫一個添加好友按鈕:
~~~
<button id="addfriend"?class="mui-btn mui-btn-blue mui-btn-link mui-pull-right">添加</button>
~~~
為了簡單演示,我們直接彈出一個輸入對話框:
~~~
// 添加好友
mui("#addfriend")[0].addEventListener('tap',function(e){
e.detail.gesture.preventDefault();
var btnArray = ['確定','取消'];
mui.prompt('請輸入你要添加的好友的用戶名:', '手機號', '邀請好友', btnArray, function(e) {
if (e.index == 0) {
var user = e.value;
conn.subscribe({
to : user,
message : "加個好友唄"
});
mui.toast('邀請發送成功!');
} else {
mui.toast('你取消了發送!');
}
});
})
~~~
> 需要說明的是如果添加好友是一個單獨的頁面,或者說所在頁面沒有和環信建立連接,依然還有進行前面說的三步連接。
**邀請接受方:**
被添加方,在 con.init 方法中調用 handlePresence 回調方法。
~~~
conn.init({
//收到聯系人訂閱請求的回調方法
onPresence : function(message) {
handlePresence(message);
}
});
//easemobwebim-sdk中收到聯系人訂閱請求的處理方法,具體的type值所對應的值請參考xmpp協議規范
var handlePresence = function(e) {
mui.toast(JSON.stringify(e));
var user = e.from;
//(發送者希望訂閱接收者的出席信息)
if (e.type == 'subscribe') {
mui.confirm('有人要添加你為好友', '添加好友', ['確定','取消'], function(e){
if (e.index == 0) {
//同意添加好友操作的實現方法
conn.subscribed({
to : user,
message : "[resp:true]"
});
mui.toast('你同意添加好友請求');
} else {
//拒絕添加好友的方法處理
conn.unsubscribed({
to : user,
message : "rejectAddFriend"
});
mui.toast('你拒絕了添加好友');
}
})
}
};
~~~
前面登陸注冊一直很順利,沒啥問題,但是做這個請求好友的時候就出問題了,我們在發送好友請求的時候,然后切換賬號登陸的時候接受不到消息。調了好久才發現一些問題:
* 我們發送好友的消息在主界面,所以我初始化了連接,接受消息的在子頁面也初始化了連接,居然有時候會有提示`onflict`,有兩種方法:第一,主界面不做任何請求的事,點擊添加好友時候,父頁面給子頁面發消息,然后子頁面執行請求添加好友;第二,所有的初始化請求放在主界面,然后收到消息給對應的子頁面發消息,為了減少請求,個人采用第二種方法。
* 當解決上面的沖突問題,為什么登錄后收不到消息?這里有個略坑的是環信文檔中查詢好友時候把`onOpened`中的這句`conn.setPresence();`屏蔽了,然后就收不到消息。查文檔[常見問題](http://docs.easemob.com/im/400webimintegration/80webimfaq)中說:
登錄之后需要設置在線狀態,才能收到消息。請檢查登錄成功后是否調用過 conn.setPresence();。
加上果然沒問題了。。。
剩下的功能我們主要看這個文檔[初始化連接](http://docs.easemob.com/im/400webimintegration/25intiate),主要是說明了初始化時候的一些回調函數的基本用法,我們這里先來看看`onPresence`,這個是**收到聯系人訂閱請求的回調方法**,基本數據類型如下:
~~~
{
"from":"xxxxxxxxxxx",
"to":"yyyyyyyyyyy",
"fromJid":"jszblog#musicbox_xxxxxxxxxxx@easemob.com",
"toJid":"jszblog#musicbox_yyyyyyyyyyy@easemob.com",
"type":"subscribe",
"chatroom":false,
"destroy":false,
"status":"加個好友唄"
}
~~~
> 這里的xxxxxxxxxxx和yyyyyyyyyyy是電話號碼,以為我是用電話作為用戶名的,出于隱私保護用字母代替。
當我們切換賬號會發現查詢好友的地方可以查到好友,下面我們就進行好友列表展示,然后就是和好友聊天咯。
### 5.數據綁定和本地緩存處理機制
當我們重新登錄的時候打印`roster`時會得到下面的`json`對象:
~~~
[{
"subscription":"from",
"jid":"jszblog#musicbox_xxxxxxxxxxx@easemob.com",
"name":"xxxxxxxxxxx",
"groups":[]
}]
~~~
為了考慮如果用戶沒有聯網或者數據不能及時更新也能夠正常看到歷史記錄,這里我們考慮做緩存,由于環信`web im`不具備緩存功能,所以我們這里采用本地存儲作為緩存的方案,本地存儲可以使用`5+`中的`storage`模塊,也可以使用`localStorage`、`sessionStorage`,由于`storage`模塊中的數據有效域不同,可在應用內跨域操作,數據存儲期是持久化的,并且沒有容量限制,這里我們采用這個方案,至于如果想把本案例中的例子用于瀏覽器端的同志,可以采用`localStorage`作緩存功能。
`html5+`中的`storage`模塊比較簡單,文檔中介紹了幾個基本方法,具體看看文檔就可以學會使用,文檔見[【storage】](http://www.html5plus.org/doc/zh_cn/storage.html)。
> plus.storage.setItem(key, value);
`plus.storage.setItem`在存儲時是以`key-value`的形式存儲,我們可以在查詢到好友信息時候,將對象轉換成字符串存儲在本地,`JSON.stringify()`將`json`對象轉換成`json`字符串。
~~~
plus.storage.setItem("roster",JSON.stringify(roster));
~~~
> plus.storage.getItem(key);
我們在子頁面通過`plus.storage.getItem`獲取存儲的字符串,然后通過`JSON.parse()`將字符串轉化成對象獲取相關信息。
~~~
var roster = plus.storage.getItem("roster");
var obj = JSON.parse(roster);
for(var i in obj){
console.log(obj[i].name);
}
~~~
我們現在要做的無非是將信息展示出來,但是這里有用的信息目前只有name,畢竟沒有上傳文件,所以也不存在頭像、昵稱、簽名這種個性化信息。如何把`json`信息展示出來前面的文章中我們是使用直接生成`dom`節點或者拼接`html`字符串,但是這種過于繁瑣,當然也有人使用【js模板引擎】,本來準備早點在文章中給一些新手介紹一下`vue.js`這種`MV-*`框架,但是考慮本文中實例的性能,暫且還是用之前用過的一個js模板引擎[artTemplate](https://github.com/aui/artTemplate),文檔戳這里:[https://github.com/aui/artTemplate](https://github.com/aui/artTemplate)。
`artTemplate`有簡潔語法版和原生語法版,就是使用語法不一樣而已,這里我使用簡潔語法版,戳這里下載——[下載地址](https://raw.githubusercontent.com/aui/artTemplate/master/dist/template.js)
為了簡單,我們采用模板中通訊錄的html結構,文檔中有這樣的一個例子:
**編寫模板:**
使用一個type="text/html"的script標簽存放模板:
~~~
<script id="test" type="text/html">
<h1>{{title}}</h1>
<ul>
{{each list as value i}}
<li>索引 {{i + 1}} :{{value}}</li>
{{/each}}
</ul>
</script>
~~~
**渲染模板:**
~~~
var data = {
title: '標簽',
list: ['文藝', '博客', '攝影', '電影', '民謠', '旅行', '吉他']
};
var html = template('test', data);
document.getElementById('content').innerHTML = html;
~~~
具體語法參考這里:[artTemplate 簡潔版語法](https://github.com/aui/artTemplate/wiki/syntax:simple)
我們可以這樣寫:
~~~
...
<div class="mui-content">
<!--內容-->
<ul id="roster-cnt" class="mui-table-view mui-table-view-striped mui-table-view-condensed"></ul>
</div>
<!--模板-->
<script id="roster-tpl" type="text/html">
{{each roster as value index}}
<li class="mui-table-view-cell" data-chatname="{{value.name}}">
<div class="mui-slider-cell">
<div class="oa-contact-cell mui-table">
<div class="oa-contact-avatar mui-table-cell">
<img src="http://placehold.it/60x60" />
</div>
<div class="oa-contact-content mui-table-cell">
<div class="mui-clearfix">
<h4 class="oa-contact-name">小青年</h4>
<span class="oa-contact-position mui-h6">湖北</span>
</div>
<p class="oa-contact-email mui-h6">
{{value.name}}
</p>
</div>
</div>
</div>
</li>
{{/each}}
</script>
...
mui.plusReady(function(){
var roster = plus.storage.getItem("roster");
// console.log(roster);
var data = {
roster: JSON.parse(roster)
}
var html = template('roster-tpl', data);
document.getElementById('roster-cnt').innerHTML = html;
})
~~~
我們其實可以直接先遍歷找到`name`然后填充就`ok`,這為了后續
方便添加昵稱、地址、頭像等個性化地址,直接使用`artTemplate`的`each`方法。
### 6.聊天消息封裝
當我們完成了前面登陸、注冊、添加好友等功能,我們就進行最重要的內容了,既然是聊天功能,當然要聊起來,不然就不叫`IM`,但是很多人一開始就太過于關注聊天這個功能,而忽略了前面的基礎過程,導致對`api`不熟悉,自然些聊天過程也是漏洞百出,代碼邏輯混亂,所以也就放棄了。本文為即時通訊第一篇,沒有介紹過多原理,也沒有介紹聊天過程的高級功能,僅作為新手入門的基礎篇介紹,后面會再深入探究更多內容。廢話不多說,我們繼續看文檔寫下面的內容。
我們先新建一個`single-chat.html`,本文不打算基于`html mui`中的頁面去構建聊天頁面,打算從零開始寫。
首先我們需要在剛剛那個通訊錄頁面里面點擊進入聊天頁面,將用戶名的值傳到聊天頁面,我們可以直接在創建的時候用拓展參數傳,或者預加載打開時用`mui.fire()`,不多說,自己參考第三小節。
我們先說說布局的問題,先上圖

對應的布局詳細代碼如下:
~~~
<style>
.chat-history-date{
display: block;
padding-top: 5px;
text-align: center;
font-size: 12px;
}
.chat-receiver,.chat-sender{
margin: 5px;
clear:both;
}
.chat-avatar img{
width: 40px;
height: 40px;
border-radius: 50%;
}
.chat-receiver .chat-avatar{
float: left;
}
.chat-sender .chat-avatar{
float: right;
}
.chat-content{
position: relative;
max-width: 60%;
min-height: 20px;
margin: 0 10px 10px 10px;
padding: 10px;
font-size:15px;
border-radius:7px;
}
.chat-content img{
width: 100%;
}
.chat-receiver .chat-content{
float: left;
color: #383838;
background-color: #f5f5f5;
}
.chat-sender .chat-content{
float:right;
color: #ffffff;
background-color: #15b5e9;
}
.chat-triangle{
position: absolute;
top:6px;
width:0px;
height:0px;
border-width:8px;
border-style:solid;
}
.chat-receiver .chat-triangle{
left:-16px;
border-color:transparent #f5f5f5 transparent transparent;
}
.chat-sender .chat-triangle{
right:-16px;
border-color:transparent transparent transparent #15b5e9;
}
</style>
<!--消息最后歷史時間-->
<p class="chat-history-date">01:59</p>
<!--接收文本消息-->
<div class="chat-receiver">
<div class="chat-avatar">
<img src="../img/chat-1.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<span>如果是接受消息,請使用.chat-receiver類,如果是發送消息,請使用.chat-sender,頭像是.chat-avatar類,內容是.chat-content類。.chat-content下如果是span標簽則為文本消息,若為img標簽則為圖片消息。</span>
</div>
</div>
<!--發送文本消息-->
<div class="chat-sender">
<div class="chat-avatar">
<img src="../img/chat-2.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<span>如果你要修改聊天氣泡的背景顏色,請修改.chat-content的background-color和.chat-triangle的border-color</span>
</div>
</div>
<!--發送圖片消息-->
<div class="chat-sender">
<div class="chat-avatar">
<img src="../img/chat-2.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<img src="../img/test.jpg"/>
</div>
</div>
~~~
我們的消息分為發送和收到兩種情況,上面是靜態效果,我們下面需要做的事獲取數據然后動態展示,現在我們先封裝一下頁面展示效果的代碼。這里我們使用兩種方法,一種是直接用`js`生成`dom`節點,這種使用于結構固定后面不需要改動的,直接用一個`js function`封裝,每次調用一行代碼就可以直接顯示內容,這樣想想都覺得很棒。
老司機,別說話,快看代碼!
~~~
/**
* @description 顯示消息
* @param {String} who 消息來源,可選參數: {params} 'sender','receiver'
* @param {Object} type 消息類型,可選參數: {params} 'text','url','img'
* @param {JSON} data 消息數據,可選參數: {params} {{el:'消息容器選擇器'},{senderAvatar:'發送者頭像地址'},{receiverAvatar:'接收者頭像地址'},{msg:'消息內容'}}
* ('text'和'url'類型的msg是文字,img類型的msg是img地址)
*/
var appendMsg = function(who,type,data) {
// 生成節點
var domCreat = function(node){
return document.createElement(node)
};
// 基本節點
var msgItem = domCreat("div"),
avatarBox = domCreat("div"),
contentBox = domCreat("div"),
avatar = domCreat("img"),
triangle = domCreat("div");
// 頭像節點
avatarBox.className="chat-avatar";
avatar.src = (who=="sender")?data.senderAvatar:data.receiverAvatar;
avatarBox.appendChild(avatar);
// 內容節點
contentBox.className="chat-content";
triangle.className="chat-triangle";
contentBox.appendChild(triangle);
// 消息類型
switch (type){
case "text":
var msgTextNode = domCreat("span");
var textnode=document.createTextNode(data.msg);
msgTextNode.appendChild(textnode);
contentBox.appendChild(msgTextNode);
break;
case "url":
var msgUrlNode = domCreat("a");
var textnode=document.createTextNode(data.msg);
if(data.indexOf('http://') < 0){
data.msg = "http://" + data.msg;
}
msgUrlNode.setAttribute("href",data.msg);
msgUrlNode.appendChild(textnode);
contentBox.appendChild(msgUrlNode);
break;
case "img":
var msgImgNode = domCreat("img");
msgImgNode.src = data.msg;
contentBox.appendChild(msgImgNode);
break;
default:
break;
}
// 節點連接
msgItem.className="chat-"+who;
msgItem.appendChild(avatarBox);
msgItem.appendChild(contentBox);
document.querySelector(data.el).appendChild(msgItem);
}
~~~
其實后面我們拓展也很容易的,只需要不斷加`type`類型就`ok`,這些都是`dom`操作的基本方法,如果對一些方法不熟悉,建議看看相關的內容。這里遵照`JSDoc+`規范還加上了使用參數提示,在`hbuilder`使用可以查看參數含義,再也不用擔心寫代碼時忘記了參數含義。
這里我們也可以用模板引擎的辦法去封裝,代碼如下:
**模板內容:**
~~~
<script id="msg-tpl" type="text/html">
<div class="chat-{{who}}">
<div class="chat-avatar">
<img src="{{avatar}}">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
{{if type=="text"}}
<span>{{msg}}</span>
{{else if type=="url"}}
<a href="{{msg}}">{{msg}}</a>
{{else if type=="img"}}
<img src="{{msg}}"/>
{{/if}}
</div>
</div>
</script>
~~~
**模板渲染:**
~~~
/**
* @description 顯示消息
* @param {String} who 消息來源,可選參數: {params} 'sender','receiver'
* @param {Object} type 消息類型,可選參數: {params} 'text','url','img'
* @param {JSON} data 消息數據,可選參數: {params} {{el:'消息容器選擇器'},{senderAvatar:'發送者頭像地址'},{receiverAvatar:'接收者頭像地址'},{msg:'消息內容'}}
* ('text'和'url'類型的msg是文字,img類型的msg是img地址)
*/
var appendMsg = function(who,type,data){
var html = template('msg-tpl', {
who: who,
type: type,
avatar: who=='sender'?data.senderAvatar:data.receiverAvatar,
msg: data.msg
});
document.querySelector(data.el).innerHTML += html;
}
~~~
大家使用也很簡單,調用方法如下:
~~~
appendMsg('sender','text',{
el: '#msg-list', //消息容器
senderAvatar: '../img/chat-1.png', //發送者頭像
receiverAvatar: '../img/chat-2.png', //接收者頭像
msg: '你好' //消息內容
})
~~~
如果大家覺得每次調用還要填寫容器id,頭像地址這種基本固定的內容很麻煩,大家也可以繼續封裝:
~~~
/**
* 消息初始化
*/
var msgInit = {
el: '#msg-list', //消息容器
senderAvatar: '../img/chat-1.png', //發送者頭像
receiverAvatar: '../img/chat-2.png', //接收者頭像
}
/**
* @description 展示消息精簡版
* @param {String} who 消息來源,可選參數: {params} 'sender','receiver'
* @param {Object} type 消息類型,可選參數: {params} 'text','url','img'
* @param {Object} msg ('text'和'url'類型的msg是文字,img類型的msg是img地址)
*/
var msgShow = function(who,type,msg){
appendMsg(who,type,{
el: msgInit.el,
senderAvatar: msgInit.senderAvatar,
receiverAvatar: msgInit.receiverAvatar,
msg: msg
});
}
~~~
調用方法很簡單:
~~~
msgShow('sender','text','你好');
~~~
兩種方法實現封裝的函數一樣,這里只是給大家演示一下對于這種動態結構的`html`的一些方法,當然只要你愿意,你可以直接用字符串拼接,或者用`<template></template>`標簽自己做一個這樣的模板引擎,或者使用使用更加方便的`mvc`或`mvvm`框架。
之所以要花大篇幅內容將這些基礎內容,是因為看到很多人代碼寫得那叫一個混亂,如果接口啥的一改,我相信這些人會瘋掉,因為代碼缺乏一定的通用性,沒有把變與不變的內容分別拿出來。當然我們上面其實有些東西沒有封裝進去,比如用戶名或者昵稱,這在群聊中是有必要的,這里只是以最簡單的例子來說明,大家可以根據自己的業務需求自由發揮。
### 7.單聊之文本消息
#### 基本思路
其實寫到這里本篇基本也算告一段落,但是考慮到很多新手對于收發消息很多還是有一些問題,我們這里就還是把文本消息發送接收寫完了再收篇。
上面我們我們講了怎么把消息展示出來,但是畢竟聊起來數據是動態的,那么發送接收數據是很重要的一步,先來寫發送消息。我們先定義一個底部的輸入框加按鈕,代碼如下:
~~~
<style type="text/css">
footer {
position: fixed;
width: 100%;
height: 50px;
min-height: 50px;
border-top: solid 1px #bbb;
left: 0px;
bottom: 0px;
overflow: hidden;
padding: 0px 50px;
background-color: #fafafa;
}
.footer-left {
position: absolute;
width: 50px;
height: 50px;
left: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 4px;
}
.footer-right {
position: absolute;
width: 50px;
height: 50px;
right: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 5px;
display: inline-block;
}
.footer-center {
height: 100%;
padding: 5px 0px;
}
.footer-center [class*=input] {
width: 100%;
height: 100%;
border-radius: 5px;
}
.footer-center .input-text {
background: #fff;
border: solid 1px #ddd;
padding: 10px !important;
font-size: 16px !important;
line-height: 18px !important;
font-family: verdana !important;
overflow: hidden;
}
footer .mui-icon {
color: #000;
}
footer .mui-icon:active {
color: #007AFF !important;
}
.footer-right span{
color: #0062CC;
line-height: 30px;
}
</style>
<div class="mui-content">
<div id="msg-list"></div>
</div>
<footer>
<div class="footer-left">
<i id='msg-choose-img' class="mui-icon mui-icon-camera" style="font-size: 28px;"></i>
</div>
<div class="footer-center">
<textarea id='msg-text' type="text" class='input-text'></textarea>
</div>
<div class="footer-right">
<span id='msg-send-text'>發送</span>
</div>
</footer>
~~~
為了代碼整潔規范,方便后期封裝,參考`hello mui`中`im-chat.html`的寫法,我們先定義一下`ui`控件對象:
~~~
// UI控件對象
var ui = {
content: mui('.mui-content'[0]),
msgList: mui('#msg-list')[0],
footer: mui('footer')[0],
msgChooseImg: mui("#msg-choose-img")[0],
msgText: mui('#msg-text')[0],
msgSendText: mui('#msg-send-text')[0]
}
~~~
發送文本消息很簡單:
~~~
// 發送文本消息
ui.msgSendText.addEventListener('tap',function(){
sendText();
})
// 發送文本
var sendText = function(){
var msg = ui.msgText.value.replace(new RegExp('\n', 'gm'), '<br/>');
var validateReg = /^\S+$/;
// 獲得鍵盤焦點
msgTextFocus();
if(validateReg.test(msg)){
// 消息展示出來
msgShow('sender','text',msg);
// 發送文本消息到環信服務器
conn.sendTextMessage({
to: chatName, //用戶登錄名,SDK根據AppKey和domain組織jid,如easemob-demo#chatdemoui_**TEST**@easemob.com,中"to:TEST",下同
msg: msg, //文本消息
type: "chat"
//ext :{"extmsg":"extends messages"}//用戶自擴展的消息內容(群聊用法相同)
});
// 清空文本框
ui.msgText.value = '';
// 恢復輸入框高度(因為我們這里是50px,你可以寫一個全局變量)
ui.footer.style.height = '50px';
// 保持輸入狀態
mui.trigger(ui.msgText, 'input', null);
// 這一句讓內容滾動起來
msgScrollTop();
}else{
mui.toast("文本消息不能為空");
}
}
~~~
這里的`msgTextFocus();`和`msgScrollTop();`是封裝的兩個方法,具體的且看下文。
再來說說收消息,我們需要在`conn.init()`配置設置收到消息的回調函數`onTextMessage`:
~~~
// 初始化連接
conn.init({
onOpened : function(){
//mui.toast("成功登錄");
conn.setPresence();
},
// 收到文本消息時的回調函數
onTextMessage : function(message) {
// console.log(JSON.stringify(message));
var from = message.from;//消息的發送者
var msg = message.data;//文本消息體
//mui.toast(msg);
// 收到文本消息在頁面展示
msgShow('receiver','text',msg);
msgScrollTop();
},
// 收到圖片消息時的回調函數
onPictureMessage : function(message) {
handlePictureMessage(message);
}
});
~~~
至此我們完成了基本的文本消息收發功能,但是有幾個細節是需要處理的,比如我們上面說的兩個函數啥意思,我們沒有解釋。
#### 獲得輸入框焦點事件和強制彈出軟鍵盤
我們如果不做處理,在輸入框失去焦點時軟鍵盤會自動收回軟鍵盤,這樣很影響聊天時候的用戶體驗。這個時候我們可以在輸入完內容,準備發送時,保持輸入狀態`mui.trigger(ui.msgText, 'input', null);`。
讓輸入框獲得焦點的方法:
~~~
// 獲得輸入框鍵盤焦點
var msgTextFocus = function(){
ui.msgText.focus();
setTimeout(function() {
ui.msgText.focus();
}, 150);
}
~~~
強制彈出軟鍵盤的方法:
~~~
// 強制彈出軟鍵盤
var showKeyboard = function() {
if (mui.os.ios) {
var webView = plus.webview.currentWebview().nativeInstanceObject();
webView.plusCallMethod({
"setKeyboardDisplayRequiresUserAction": false
});
} else if(mui.os.android) {
var Context = plus.android.importClass("android.content.Context");
var InputMethodManager = plus.android.importClass("android.view.inputmethod.InputMethodManager");
var main = plus.android.runtimeMainActivity();
var imm = main.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(0,InputMethodManager.SHOW_FORCED);
}
};
~~~
#### 聊天消息高度調整
聊天消息如何發送或者收到一條自己往上滾動呢?我們看qq消息就是最后一條消息就會自動出現在輸入框之上,調整方法是使用`scrollTop`方法,通過計算`scrollHeight`和`offsetHeight的高度,實現調整。對這些高度不理解?看這里:
* [HTML 獲取屏幕、瀏覽器、頁面的高度寬度](http://www.cnblogs.com/polk6/p/5051935.html)
* [深入理解高度。獲取屏幕、webview、軟鍵盤高度](http://ask.dcloud.net.cn/article/205)
其實這個地方有很多技術細節,比如消息高度雖然可以獲取,但是要實現局部滾動,那么必須禁止瀏覽器默認的滾動模式,具體可以看看這篇文章的實現原理[淺議內滾動布局](https://isux.tencent.com/inner-scroll-layout.html)
具體css樣式設置方法:
~~~
html,
body {
height: 100%;
margin: 0px;
padding: 0px;
overflow: hidden;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
.mui-content{
height: 100%;
padding: 44px 0px 50px 0px;
overflow: auto;
background-color: #eaeaea;
}
#msg-list {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
~~~
調用的函數封裝如下:
~~~
// 消息滾動
var msgScrollTop = function(){
ui.msgList.scrollTop = ui.msgList.scrollHeight + ui.msgList.offsetHeight;
}
~~~
#### 輸入框高度如何自適應
不多說直接上代碼:
~~~
// 輸入框監聽事件
ui.msgText.addEventListener('input', function(event) {
msgTextFocus();
ui.footer.style.height = this.scrollHeight + 'px';
});
~~~
#### 解決長按導致致鍵盤關閉的問題
~~~
// 解決長按“發送”按鈕,導致鍵盤關閉的問題;
ui.msgSendText.addEventListener('touchstart', function(event) {
msgTextFocus();
event.preventDefault();
});
ui.msgSendText.addEventListener('touchmove', function(event) {
msgTextFocus();
event.preventDefault();
});
~~~
當做到這里我們基本要講解的夠新手去理解了,但是對于項目功能實現來說,遠遠不夠,畢竟只是文字發送接收,那么圖片、語音、地址等等高級功能呢,我們這篇文章限于篇幅不可能一一道來,只能后面再做補充。這里希望更多人參與到其中進行貢獻。這里可以放出地址了,詳情代碼請關注這里:[https://github.com/zhaomenghuan/mui-demo/tree/master/easemobIM](https://github.com/zhaomenghuan/mui-demo/tree/master/easemobIM)。后期功能拓展和bug修復都貴提交到這里,歡迎大家貢獻。
## 寫在后面
由于這段時間確實有點忙,這篇文章也花了很多時間去碼字,去修改,改了很多次,才有這篇文章,希望能夠給新手一些啟示和幫助吧!本文不是著重講環信sdk怎么用,而是講解這個過程中可能會遇到的一些問題和實現思路,所以不建議新手直接拿最后的代碼改之類的,還是看懂了思路再說,所以至于這個IM更多的功能后期會不會繼續開發,暫時是未知數,所以大家不要等待,歡迎大神多多貢獻分享相關代碼,這樣方便更多人學習使用。
這里想和大家簡單說下,不要所有的問題直接私聊我,我時間精力有限,個人覺得不回復不好,所以我不會看到消息裝著沒看到,但是也不可能一一去回復,畢竟時間精力上也有限,我也需要不斷學習,所以不希望過多的被打擾。大家有問題建議去論壇先搜搜答案,看看官方文檔自己解決,大家確實有解決不了的問題,可以在群里尋求幫助或者給我發消息。提問前建議把問題描述清楚,想要實現什么,然后現在的實現思路,最好附上代碼說明或者發測試文件,這樣也方便解決問題。很多人說直接是直接是根據官方demo改的,說啥bug,很多時候聊到最后發現是他自己的原因,這種情況真的很浪費彼此時間。在此聲明以后一上來直接要幫忙寫代碼的,或者讓我發一遍文檔地址的等等這種可以自己可以解決的問題,請原諒我直接果斷拒絕,我理解新手理解小白初入門的盲目,但是建議不要去依賴,要自己去嘗試,不懂再問,不要還沒有去了解去查資料,直接一上來求帶這種要求。

本人博客歡迎轉載!但請注明出處!本人博客若有侵犯他人之處,望見諒,請聯系我。希望互相關注,互相學習 --[PheonixHkbxoic](http://www.cnblogs.com/PheonixHkbxoic/)