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...
相关文章
相关标签/搜索