自制聊天系统

今天是星期五,周末又到了,又是一个人在家路代码的黄金时期,这次给大家分享一个用js做的在线聊天界面。

1 参数设置

var jid = "";   // 当前登录的JID  

var current_target_name = '';   // 当前聊天对象
var last_target_name = '';   // 上次聊天对象

var max_msg_count = 50;   // 最大储存信息数量
var isFirst = true;   // 是否首次登入

//通讯录
var friends_name_arr = [];//name1,name2...  这个数组是装好友名字的  ['张三',‘李四’]
//记录好友上线状态
var online_friends_arr = [];//{name1,status1},{name2,status2} ....  对象包括名字和状态(在线,离线)

// 在线好友和离线好友的数组
var online_friends_names = [];
var offline_friends_names = [];

//登陆用户与密码
var username_str='';   // 这是啥 后面会解释
var username = '';
var password = '';

//消息盒子 用于保存离线消息(不在线时,受到的好友信息会放到这里)
var message_box = [];

相信通过上面的参数大家已经看出来了这是哪一处戏,无非就是一个即使聊天的工具。

2 Html页面

 <!-- 好友列表区域 -->
        <div class="user-list-canvas">
            <!-- 搜索区域 -->
            <div class="search-canvas">
                <div class="search-img"></div>
                <input class="search-tool" type="text" placeholder="搜索">
            </div>
            <!-- 列表区域 -->
            <ul class="list-canvas">
                
            </ul>
            <script type="text/template" id="friend-row">
                <li class="friend-canvas {name}">  // 在每个li上标记该好友的名字
                    <div class="friend-logo">
                        <span class="friend-logo-lab">{logo}</span>
                        <span class="noread-num num-{noReadNum}">{noReadNum}</span>  //未读消息数量
                    </div>
                    <div class="friend-info">
                        <span class="username" style="display: none">{name}</span>
                        <span class="nickName">{nickName}</span>  //昵称
                        <span class="last-message">{lastMessage}</span>  //受到的最后一条消息
                    </div>
                </li>
            </script>
        </div>
  1. .search-tool是一个搜索框,可以进行选择。
  2. .list-canvas 是好友列表
    可能有人看不懂这个script里面写的是啥,其实就是一个简单的模板用于渲染数据,后面会说明。
<!-- 聊天区域 -->
        <div class="chat-content-canvas">
            <div class="chat-content-title">
                <!-- 记录当前聊天对象 -->
                <div class="current-name"></div>
                <div class="status-logo" style="display: none;"></div>
                <div class="current-status" style="display: none;"></div>
                <div class="clear-content"></div>
            </div>
             <!-- 记录当前聊天内容 -->
            <div class="chat-content-panel">
            </div>

            <!-- 发出信息模板 -->
            <script type="text/template" id="send-message-model">
                <div class="message-row">
                    <div class="send-message-style">{txt}</div>
                    <div class="message-info send-message-info">
                        <span>{date}</span>
                        <span></span>
                    <div>
                </div>
            </script>
            <!-- 接收信息模板 -->
            <script type="text/template" id="receive-message-model">
                <div class="message-row">
                    <div class="receive-message-style">{txt}</div>
                    <div class="message-info receive-message-info">
                        <span>{date}</span>
                        <span></span>
                    <div>
                </div>
            </script>

        </div>
        <!-- 书写区域 -->
        <div class="send-content-canvas">
            <textarea class="message-canvas" placeholder="请输入要发送的信息"></textarea>
            <div class="send-button">发送</div>
        </div>
    </div>

.send-content-canvas是书写区域,点击发送会把消息发出去,渲染到发送消息的模板。
好了,页面就是这样的,已经很简洁了,下面来看功能实现的代码。

3 Js代码

// 连接状态改变的事件  
function onConnect(status) {  
    if (status == Strophe.Status.CONNFAIL) {  
        alert("连接失败!");  
    } else if (status == Strophe.Status.AUTHFAIL) {  
        alert("登录失败!"); 
    } else if (status == Strophe.Status.DISCONNECTED) {  
        loginOutFunc();
    } else if (status == Strophe.Status.CONNECTED) {  
        loginInFunc();
    }  
}

上面展示了几种状态的函数执行,我们来看看登录后如何操作。

//用户登陆
function loginInFunc(){
    connected = true; 
    connection.send($pres().tree());

    $('#btn-login').html('断开');   //这是一个切换按钮,用于登录和登出。
    $("#chat-panel").show();     // 聊天面板展开
    $("#loading-background").hide();   //loading效果消失

    // 当接收到<message>节,调用onMessage回调函数  
    connection.addHandler(onMessage, null, 'message', null, null, null);
    // 当接收到<presence>节,调用onPresenceOn回调函数  
    connection.addHandler(onPresenceOn, null, 'presence', null, null, null);   **这2步先不管**

    //上线状态
    changeOnlineStatus('online');
    //获取好友列表
    get_roster();
}

登录后会拉取好友信息渲染,简单说渲染之后就会直到那些好友在线,那些不在线。

// 好友列表
var friend_list_data = [];   里面存放的是person
var friend_arr = [];
function onRoster(iq) {
    $(".list-canvas").html('');
    friends_name_arr = [];
    friend_list_data = [];

    $(iq).find('item').each(function () {
        var jid = $(this).attr('jid'),
            // name = $(this).attr('name')!=undefined ? $(this).attr('name') : jid.substring(0,jid.indexOf('@')),
            name = jid.substring(0,jid.indexOf('@')),
            nickName = $(this).attr('name')?$(this).attr('name'):name;

            //构建了一个person对象
            var person = new Person(name.charAt(0).toUpperCase() , name , nickName , 'offline');

            $.each(message_box, function(index, val) {
                if(val.from == name){
                    person.addNoReadNum();
                }
            });

            friend_list_data.push(person);

            //记录好友name数组
            friends_name_arr.push(name);
    });
    //数据都是通过iq传递过来的

    //初始化,获取未上线的好友名称数组
    $.each(friends_name_arr, function(index, val) {
         if(online_friends_names.indexOf(val) == -1){
            offline_friends_names.push(val);
         }
    });
    offline_friends_names.sort();//排序

    //填充好友
    fillFriendTemplate();
    update_friend_status();
    return true;
}

$.each(message_box, function(index, val) {
if(val.from == name){
person.addNoReadNum();
}
}); 遍历离线消息,如果该名字相等,这个人的未读信息加1.

$.each(friends_name_arr, function(index, val) {
if(online_friends_names.indexOf(val) == -1){
offline_friends_names.push(val);
}
}); 将所有好友名单,分成online和offline的。

问题来了:现在我们已经获得在线的friend_list_data和friend_name_arr,那么如何将它渲染到页面上呢,还有好友的的状态会改变的,在线的会下线,离线的会上线,如何实现实时监测。

先来看模板渲染把

//填充模板 填充好友模板
//friend_list_data ==> Person {logo: "A", name: "alice", status: "offline", lastMessage: "", message_arr: Array[0]}
function fillFriendTemplate(){
    // 获取好友模板html
    var html = $("#friend-row").html();
    friend_arr = [];

    //对数组进行排序  按名字进行排序  by是一个排序函数  这里不贴出来了
    friend_list_data.sort(by("name"));

    $.each(friend_list_data, function(index, val) {
        friend_arr.push(formatTemplate(val , html));
    });

    // 这一步把.list-canvas的好友列表渲染出来了
    $('.list-canvas').append(friend_arr.join(''));

    //渲染最后一条信息
    $.each(friends_name_arr, function(index, val) {
        var key = val<username_str ? (val+"_"+username_str) : (username_str+"_"+val);
        var messages = getLocalMessages(key);
        if(messages){
            $("."+val).find('.last-message').html(messages[0].txt);
        }
    });
}

我们来看看formatTemplate干了啥

function formatTemplate(dta, tmpl) {  
    var format = {  
        name: function(x) {  
            return x  
        }  
    };  
    return tmpl.replace(/{(\w+)}/g, function(m1, m2) {  
        if (!m2)  
            return "";  
        return (format && format[m2]) ? format[m2](dta[m2]) : dta[m2];  
    });  
}

这是一个很好的模板替换函数,还记得之前的script里面写html吗,就是通过这种方式替换的。

$.each(friends_name_arr, function(index, val) {
var key = val<username_str ? (val+""+username_str) : (username_str+""+val);
var messages = getLocalMessages(key);
if(messages){
$("."+val).find('.last-message').html(messages[0].txt);
}
}); 这一步是把每个好友和自己最后一次聊天的内容放到.last-message里面

这里我们用到了localstorage缓存,看看是如何写的把

//获取本地消息
function getLocalMessages(name){
    return JSON.parse(localStorage.getItem(name));
}

非常简单的一段代码

接下来我们看看怎么实现上下线的监听,有点像qq呀

//监听presence
function onPresenceOn(pres){
    var from_jid = $(pres).attr('from');
    var name = from_jid.substring(0,from_jid.indexOf('@'));
    var status = $(pres).find('status').text();

    //好友下线
    if($(pres).attr('type') == "unavailable"){
        ********
    }else{
        ********
    }

    return true;
}

还记得这段代码么

// 当接收到<presence>节,调用onPresenceOn回调函数  
    connection.addHandler(onPresenceOn, null, 'presence', null, null, null);  

就是在这个代码里面我们能听到qq里面的敲门声。其实就是监听好友上下线的。。

//好友下线
    if($(pres).attr('type') == "unavailable"){
        var dom_str = '.'+name;
        //修改好友状态-下线
        update_status_action(name , 'offline');  
        //更新上下线数组
        if(offline_friends_names.indexOf(name) == -1){
            online_friends_names.splice(online_friends_names.indexOf(name) , 1);
            offline_friends_names.push(name);
            offline_friends_names.sort();
        }
        if($(dom_str).hasClass('selected-friend')){
            $(dom_str).removeClass('selected-friend');
    }

上面是这种情况,当前选中的好友的如果下线,会把select-friend类去掉。

/好友上线
       
        online_friends_arr.push({'name':name , 'status':status});
        //更新上下线数组
        if(online_friends_names.indexOf(name) == -1 && (name!=username_str)){
            offline_friends_names.splice(offline_friends_names.indexOf(name) , 1);
            //记录上线好友名称
            online_friends_names.push(name);
            online_friends_names.sort();//排序
       }
      update_status_action(name,status);

还记得我们之前提到的messagebox把,它记录了所有的离线消息,那么设想这样一个情景:
好友A不在线,我给他发了一条信息,那么这条信息将会被存入到messagebox,这就是所谓的离线缓存。继续看
message_box = [{from:from_name , to:to_name , txt:txt_msg , date:date}] 谁发的,发给谁,信息内容,时间

function onMessage(msg) {
    // 解析出<message>的from、type属性,以及body子元素  
    var from = msg.getAttribute('from');
    var from_name = from.substring(0,from.indexOf('@'));

    var to = msg.getAttribute('to');
    var to_name = to.substring(0,to.indexOf('@'));

    var type = msg.getAttribute('type');
    var elems = msg.getElementsByTagName('body');

    var body = elems[0];
    var txt_msg = Strophe.getText(body);

    var date = new Date().format("yyyy-MM-dd hh:mm:ss");

    //离线 保存至信息箱
    if(type == "chat"){
        if(friend_list_data.length == 0){
            message_box.push({from:from_name , to:to_name , txt:txt_msg , date:date});
        }else{
            if(elems.length > 0){
                var person = findPerson(from_name);
                
                person.message_arr.push({from:from_name , to:to_name , txt:txt_msg , date:date});

                //消息已收到
                connection.send($msg({to: from}).c('received', {xmlns: 'urn:xmpp:receipts'}));
            }
            //判断是否是当前聊天窗口对象发送过来的信息
            update_message_action({from:from_name , to:to_name , txt:txt_msg , date:date} , (current_target_name===from_name?true:false));
        }

        localSaveMessage({from:from_name , to:to_name , txt:txt_msg , date:date});
    }else{
        // 信息反馈
    }
    return true;
}

onMessage这个东西很眼熟把,就像socket接收到message一样。

var from = msg.getAttribute('from');
var from_name = from.substring(0,from.indexOf('@')); 发送消息人的name
var to = msg.getAttribute('to');
var to_name = to.substring(0,to.indexOf('@')); 发给谁
var type = msg.getAttribute('type'); // 消息发送类型
var elems = msg.getElementsByTagName('body');
var body = elems[0];
var txt_msg = Strophe.getText(body); // 消息内容
var date = new Date().format("yyyy-MM-dd hh:mm:ss");

上面代码有一处位置是错的

if(friend_list_data.length == 0){
            message_box.push({from:from_name , to:to_name , txt:txt_msg , date:date});
        }

好友列表为0的时候,往离线盒子推送,显然是错的。这里应该判断to_name的状态是否离线,离线就发送到离线邮箱。

// 查找好友(非搜索)
function findPerson(name){
    var idx = -1;
    $.each(friend_list_data, function(index, val) {
        if(val.name == name){
            idx = index;
        }
    });
    return friend_list_data[idx];
}
if(findPerson(to_name).getStatus() == 'offline'){
       message_box.push({from:from_name , to:to_name , txt:txt_msg , date:date});
}

如果是在线好友,就不用发送到离线信箱,进行下面的操作即可。

//消息渲染
function update_message_action(msg , flag){
    var html = '';
    
    // flag判断消息是否来自当前聊天对象
    if(flag){
        // 发出信息   send-message-model 
        if(msg.from == username_str){
            html = $('#send-message-model').html();
            var msg_context = formatTemplate(msg , html);
            $('.chat-content-panel').append(msg_context);
            $("."+msg.to).find('.last-message').html(msg.txt);   // 发给谁,谁的最后一条消息就是这个
        }else{
        // 收到信息  receive-message-model
            html = $('#receive-message-model').html();
            var msg_context = formatTemplate(msg , html);
            $('.chat-content-panel').append(msg_context);
            $("."+msg.from).find('.last-message').html(msg.txt);
        }
        //滚动条置底
        $('.chat-content-panel').get(0).scrollTop = $('.chat-content-panel').get(0).scrollHeight;
    }else{
        var person = findPerson(msg.from);
        person.addNoReadNum();
        $("."+msg.from).find('.noread-num').html(person.getNoReadNum());
        $('.'+person.name).find('.noread-num').removeClass('num-0');
        $("."+msg.from).find('.last-message').html(msg.txt);
    }
}

无论发送还是接收,$('.chat-content-panel')都会追加内容。由于写作匆忙,很多位置还未更正,下回我会同步到github上,供大家下载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容