mui初級入門教程(五)— 聊聊即時通信(IM),基於環信 web im SDK

文章來源:小青年原創
發佈時間:2016-06-15
關鍵詞:mui,環信 web im,html5+,im,頁面傳值,緩存
轉載需標註本文原始地址: http://zhaomenghuan.github.io...

寫在前面

感受自從qq、微信這種APP用多了,如今都沒啥人發短信了,如今什麼APP都想加入IM的功能,曾經有段時間在折騰本身擼一個聊天的東西,也嘗試過不少平臺,今天這裏給你們介紹一下從零開始本身作一個聊天的app功能。由於以前幫朋友作過一個基於環信的聊天功能,這裏就以環信的平臺爲例舉個例子說明。這篇文章注意想講解一下集成這種第三方的通常實現方法,不會一會兒就把全部的功能都集成,由於以前作環信主要是在微信上用,因此用的是環信的Web IM,遇到了蠻多坑,此次打算用dcloud這邊的mui從新集成,因此在沒有徹底作完以前,因此也不知道有些坑具體可以在有限的時間內解決,本文僅供參考,歡迎你們去實踐檢驗。在寫這篇文章以前先貼一個Dcloud論壇中的資源帖,【即時通訊、im問題彙總】css

準備工做

1.註冊帳號

咱們要先去環信官網註冊一個帳號,而後在後臺建立一個應用,由於咱們後面在作功能的時候能夠用後面發送消息及圖片來測試收消息,用戶管理在後臺也能夠看得一清二楚。html

clipboard.png

建立成功後找到應用標識(AppKey),這個在後期配置中會用到。前端

2.下載SDK

http://www.easemob.com/downlo...
這裏咱們使用的是Web IM,因此下載的SDKWeb IM版本,下載以後咱們會看到一個演示demo,因爲這個是pc版本,和咱們需求不一致,因此咱們只須要關心sdk目錄下的文件和sdk集成須要修改的配置文件easemob.im.config.jsvue

|---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/40...html5

項目實戰

因爲這篇重在在於如何使用第三方開發IM,感受說再多也誒有意義,直接上代碼說明。不講解過多的原理、細節,只講究開發流程。node

1.用戶註冊功能

首先咱們在hbuilder中先新建一個項目easemobIM,而後把環信sdk文件夾和配置文件拷貝到咱們的工程中。爲了節約時間,下面的功能演示我是根據官方登陸模板改的。
html/reg.htmlandroid

<!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>

這是註冊頁面的代碼,咱們首先要引入環信的sdkeasemob.im.config.js,而且將easemob.im.config.js中的appkey換成本身的,而後根據用戶名/密碼/暱稱註冊環信 Web IM,提交註冊的代碼爲:ios

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。git

2.用戶登陸功能

有了註冊頁面的經驗,咱們寫登陸頁面也很簡單,頁面佈局腳本和其餘與登陸邏輯無關的代碼我這裏不貼了,你們在我最後給的地址上下載完整代碼,這裏只講解基本基本思路。環信登陸優兩種方法,一種是經過實例化new Easemob.im.Connection()創建鏈接,一種是使用工具類Easemob.im.Helper.login2UserGrid(options),咱們剛剛註冊就是使用了工具類,爲了便於你們後面的學習,咱們在這裏把兩種方法都說一下:github

實例化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()方法中須要配置的屬性是 userpwd,這和咱們註冊時的有區別,要注意哦!

這裏須要說明的是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.htmlmui.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事件才完成,咱們能夠選擇合適的時機發送或者接受參數。

這裏須要說明的是若是你想localstorageStorage等本地存儲傳值,徹底能夠不用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();屏蔽了,而後就收不到消息。查文檔 常見問題 中說:

登陸以後須要設置在線狀態,才能收到消息。請檢查登陸成功後是否調用過 conn.setPresence();。
加上果真沒問題了。。。

剩下的功能咱們主要看這個文檔 初始化鏈接,主要是說明了初始化時候的一些回調函數的基本用法,咱們這裏先來看看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模塊,也可使用localStoragesessionStorage,因爲storage模塊中的數據有效域不一樣,可在應用內跨域操做,數據存儲期是持久化的,而且沒有容量限制,這裏咱們採用這個方案,至於若是想把本案例中的例子用於瀏覽器端的同志,能夠採用localStorage做緩存功能。

html5+中的storage模塊比較簡單,文檔中介紹了幾個基本方法,具體看看文檔就能夠學會使用,文檔見 【storage】

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/artTem...
artTemplate有簡潔語法版和原生語法版,就是使用語法不同而已,這裏我使用簡潔語法版,戳這裏下載—— 下載地址

爲了簡單,咱們採用模板中通信錄的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 簡潔版語法

咱們能夠這樣寫:

...
<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,這爲了後續
方便添加暱稱、地址、頭像等個性化地址,直接使用artTemplateeach方法。

6.聊天消息封裝

當咱們完成了前面登錄、註冊、添加好友等功能,咱們就進行最重要的內容了,既然是聊天功能,固然要聊起來,否則就不叫IM,可是不少人一開始就太過於關注聊天這個功能,而忽略了前面的基礎過程,致使對api不熟悉,天然些聊天過程也是漏洞百出,代碼邏輯混亂,因此也就放棄了。本文爲即時通信第一篇,沒有介紹過多原理,也沒有介紹聊天過程的高級功能,僅做爲新手入門的基礎篇介紹,後面會再深刻探究更多內容。廢話很少說,咱們繼續看文檔寫下面的內容。

咱們先新建一個single-chat.html,本文不打算基於html mui中的頁面去構建聊天頁面,打算從零開始寫。

首先咱們須要在剛剛那個通信錄頁面裏面點擊進入聊天頁面,將用戶名的值傳到聊天頁面,咱們能夠直接在建立的時候用拓展參數傳,或者預加載打開時用mui.fire(),很少說,本身參考第三小節。

咱們先說說佈局的問題,先上圖

clipboard.png

對應的佈局詳細代碼以下:

<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>標籤本身作一個這樣的模板引擎,或者使用使用更加方便的mvcmvvm框架。

之因此要花大篇幅內容將這些基礎內容,是由於看到不少人代碼寫得那叫一個混亂,若是接口啥的一改,我相信這些人會瘋掉,由於代碼缺少必定的通用性,沒有把變與不變的內容分別拿出來。固然咱們上面其實有些東西沒有封裝進去,好比用戶名或者暱稱,這在羣聊中是有必要的,這裏只是以最簡單的例子來講明,你們能夠根據本身的業務需求自由發揮。

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 muiim-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的高度,實現調整。對這些高度不理解?看這裏:

其實這個地方有不少技術細節,好比消息高度雖然能夠獲取,可是要實現局部滾動,那麼必須禁止瀏覽器默認的滾動模式,具體能夠看看這篇文章的實現原理淺議內滾動佈局

具體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/zhaomenghu...。後期功能拓展和bug修復都貴提交到這裏,歡迎你們貢獻。

寫在後面

因爲這段時間確實有點忙,這篇文章也花了不少時間去碼字,去修改,改了不少次,纔有這篇文章,但願可以給新手一些啓示和幫助吧!本文不是着重講環信sdk怎麼用,而是講解這個過程當中可能會遇到的一些問題和實現思路,因此不建議新手直接拿最後的代碼改之類的,仍是看懂了思路再說,因此至於這個IM更多的功能後期會不會繼續開發,暫時是未知數,因此你們不要等待,歡迎大神多多貢獻分享相關代碼,這樣方便更多人學習使用。


若是有項目需求,歡迎私聊。承接各類前端項目,同時若是有功能定製,代碼優化等需求也能夠商量,算髮個小廣告吧,畢竟我也要生活,要掙錢娶媳婦養家餬口。

寫文章不容易,也許寫這些代碼就幾十分鐘的事,寫一篇你們好接受的文章或許須要幾天的醞釀,而後加上幾天的碼字,累並快樂着。若是文章對您有幫助請我喝杯咖啡吧!進行贊助的同窗私信留下大家的聯繫方式,後期發文章會單獨郵件通知,有開發的問題也能夠私聊,有相關功能需求,能夠考慮優先寫文分享。在此特別感謝以前給予贊助的同窗,名單有保存,後期在博客會有公示。


近期在segmentfault講堂開設了一場關於 html5+ App開發工程化實踐之路的講座,會講到5+ 開發中高性能的優化方案以及使用如何結合Vue.js進行開發,歡迎前來圍觀: https://segmentfault.com/l/15...
相關文章
相關標籤/搜索