最近一直在开发博客小程序,最近开发好友系统和实时会话系统。其实众所周知会话系统正常的业务逻辑应该是用户A给用户B发送一个消息,用户A发送后用户B马上可以接收到并在页面渲染出来,而且最新的消息应该是在页面最底部。那要实现这个实时会话有什么方法呢?我们通过本系列文章可以学到什么知识点呢?我们可以先看看本系列文章涉及的知识点:
http轮询
本篇文章将会针对http轮询实现会话系统来讲解,下一篇开始将会使用websocket改写实现真正的实时会话系统。实际上会话系统最简单的方式是http轮询:用户发送信息时实现一个http接口保存用户聊天信息,然后在客户端实现一个定时器,定时获取用户A与用户B的聊天信息,并且重新渲染聊天界面。我们看下使用轮询的业务逻辑:
轮询方法实际上很简单,但是为什么我们一般不会推荐使用http轮询实现实时会话系统呢?因为轮询的缺点是显而易见的,举个很简单的例子:我们在客户端开了个1秒/次的定时器,每秒钟向服务端请求聊天数据,但是这大部分的请求可能都是没有新消息发送与接收的,所以说轮询最大的缺点就是定时器定时请求大部分都是无用的,所以会极大的浪费和消耗带宽和服务器资源。接下来我们可以来实战看下效果,首先我们看下客户端会话界面的效果:输入框可以输入聊天信息点击发送可以发送文本信息,点击+可以选择相册图片发送。我们可以简单看下客户端界面渲染代码,实际上就是通过for循环渲染聊天数据,将好友的信息渲染在左边,自己发送的信息渲染在右边:
<view class="news" bindtap='outbtn'>
<view class="historycon">
<scroll-view scroll-y="true" class="history" scroll-top="{{scrollTop}}">
<block wx:for="{{newslist}}" wx:key>
<view wx:if="{{item.flagtime}}" class="created_date">{{item.created_date}}</view>
<!--自己的消息 -->
<view class="chat-news" wx:if="{{item.username == chatInfo.userInfo.username}}">
<view style="text-align: right;padding-right: 20rpx;">
<text class="name">{{ item.username }}</text>
<image class='new_img' src="{{item.useravatar}}"></image>
</view>
<view class='my_right'>
<block wx:if="{{item.chat_type==0}}">
<view class='new_txt'>{{item.content}}</view>
</block>
<block wx:if="{{item.chat_type==1}}">
<image class="selectImg" src="{{item.content}}" data-src="{{item.content}}" lazy-load="true" bindtap="previewImg"></image>
</block>
</view>
</view>
<!-- 别人的消息 -->
<view class="chat-news" wx:else>
<view style="text-align: left;padding-left: 20rpx;">
<image class='new_img' src="{{item.useravatar? item.useravatar:'/images/defule.png'}}"></image>
<text class="name">{{ item.username }}</text>
</view>
<view class='you_left'>
<block wx:if="{{item.chat_type==0}}">
<view class='new_txt'>{{item.content}}</view>
</block>
<block wx:if="{{item.chat_type==1}}">
<image class="selectImg" src="{{item.content}}" data-src="{{item.content}}" lazy-load="true" bindtap="previewImg"></image>
</block>
</view>
</view>
</block>
</scroll-view>
</view>
</view>
<view id="flag"></view>
<!-- 聊天输入 -->
<view class="message">
<view wx:if="{{loading}}">
<i-load-more tip="{{tip}}" loading="{{loading}}" />
</view>
<i-toast id="toast" />
<form bindreset="cleanInput" class="sendMessage">
<input type="text" placeholder="请输入聊天内容.." value="{{massage}}" bindinput='bindChange' disabled="{{loading}}"></input>
<view class="add" bindtap='increase'>+</view>
<button type="primary" bindtap='send' form-type="reset" size="small" button-hover="blue" disabled="{{loading}}">发送</button>
</form>
<view class='increased {{aniStyle?"slideup":"slidedown"}}' wx:if="{{increase}}">
<view class="image" bindtap='chooseImage'>相册 </view>
</view>
</view>
我们可以看看界面的一般效果:
刚才已经说过了这个界面同时有一个我轮询事件,我这里5秒进行一次轮询,获取用户最新聊天记录然后重新渲染页面,我们看下代码:
getIntervalChat: function (that) {
interval = setInterval(function () {
wx.request({
url: utils.basePath + '/article/v1/getOnlineInfo',
method: "post",
data: {
chatInfo: that.data.chatInfo
},
header: {
'content-type': 'application/json'
},
success(res) {
if (res.data.status == 200) {
var data = res.data.payload.data;
data[0].flagtime = true;
for (var i = 1; i < data.length; i++) {
var currenttime = new Date(data[i].created_date).getTime();
var begintime = new Date(data[i - 1].created_date).getTime();
currenttime - begintime > 1000 * 60 ? data[i].flagtime = true : data[i].flagtime = false;
}
that.setData({
onlineInfo: data
});
}
}
});
}, 5000);
},
实际上逻辑很简单,就是调取API获取聊天数据,然后渲染界面。为了布局的美观性,我加了个判断,判断本条聊天记录与上一条信息是否发送时间超过1分钟,如果在1分钟以内,则时间不会重复渲染。类似QQ和微信那样的聊天方式。那接下来我们需要实现三个API:用户聊天数据保存API、用户聊天数据获取API、图片上传API。图片上传其实之前已经专门写过一篇文章介绍过了,可以自行去查看:Node上传文件(1)。接下来我们先看下聊天数据保存API,其实就是将发送信息的用户与接受信息的用户以及聊天信息进行存取,然后查询新的聊天数据返回,这里贴下关键代码:
async.waterfall([
function (callback) {
connection.beginTransaction(function (err) {
return callback(err);
});
},
function (callback) {
var sql = 'insert into online_chat set ?';
var value = {
friendphone: data.friendInfo.account,
friendname: data.friendInfo.username,
friendavatar: data.friendInfo.avatar,
app_sid: data.friendInfo.app_sid,
userphone: data.userInfo.account,
username: data.userInfo.username,
useravatar: data.userInfo.avatar,
created_date: new Date(),
status: 1,
content: data.chat_content,
chat_type: data.chat_type
};
connection.query(sql, value, function (err, result) {
if (err) {
return callback(err);
}
if (result.affectedRows == 0) {
return callback('聊天出现故障!');
}
return callback(null, '保存聊天记录成功!');
});
},
function (release_info, callback) {
var sql = 'select id, friendphone, friendname, friendavatar, app_sid, DATE_FORMAT(created_date, "%Y-%m-%d %H:%i:%s") as created_date, userphone, username, useravatar, content, chat_type from online_chat ' +
'where (friendphone = ? and userphone = ?) or (friendphone = ? and userphone = ?)';
var value = [data.friendInfo.account, data.userInfo.account, data.userInfo.account, data.friendInfo.account];
connection.query(sql, value, function (err, result) {
if (err) {
return callback(err);
}
var del_info = result && result.length > 0 ? result : null;
if (!del_info) {
return callback(null, true, []);
}
return callback(null, true, del_info);
});
}
], function (DbErr, isSuccess, uidOrInfo) {
if (DbErr || !isSuccess) {
connection.rollback(function () {
connection.release();
});
return cb(DbErr);
}
connection.commit(function (e) {
if (e) {
connection.rollback(function () {
connection.release();
});
return cb(e);
}
connection.release();
cb(null, uidOrInfo);
});
});
接下来看下聊天数据获取API,这个API实际上就是查询两个好友间的聊天记录,然后通过两个账号分别查询用户的基本信息如头像昵称等,一样贴下关键代码:
async.waterfall([
function (callback) {
connection.beginTransaction(function (err) {
return callback(err);
});
},
//通过friendphone查询好友信息
function (callback) {
var sql = 'select username, avatar from users where account = ? and app_sid = ?';
var value = [data.friendphone, data.app_sid];
connection.query(sql, value, function (err, result) {
if (err) {
return callback(err);
}
if (!result[0]) {
return callback('用户不存在!');
}
data.friendname = result[0].username;
data.friendavatar = result[0].avatar;
return callback(null, 200);
});
},
//通过userphone查询好友信息
function (info, callback) {
var sql = 'select username, avatar from users where account = ? and app_sid = ?';
var value = [data.userphone, data.app_sid];
connection.query(sql, value, function (err, result) {
if (err) {
return callback(err);
}
if (!result[0]) {
return callback('用户不存在!');
}
data.username = result[0].username;
data.useravatar = result[0].avatar;
return callback(null, 200);
});
},
function (release_info, callback) {
var sql = 'select id, friendphone, friendname, friendavatar, app_sid, DATE_FORMAT(created_date, "%Y-%m-%d %H:%i:%s") as created_date, userphone, username, useravatar, content, chat_type from online_chat ' +
'where (friendphone = ? and userphone = ?) or (friendphone = ? and userphone = ?)';
var value = [data.friendphone, data.userphone, data.userphone, data.friendphone];
connection.query(sql, value, function (err, result) {
if (err) {
return callback(err);
}
var del_info = result && result.length > 0 ? result : null;
if (!del_info) {
return callback(null, true, []);
}
return callback(null, true, del_info);
});
}
], function (DbErr, isSuccess, uidOrInfo) {
if (DbErr || !isSuccess) {
connection.rollback(function () {
connection.release();
});
return cb(DbErr);
}
connection.commit(function (e) {
if (e) {
connection.rollback(function () {
connection.release();
});
return cb(e);
}
connection.release();
cb(null, uidOrInfo);
});
});
图片上传我这里在上传图片的同时加了一个鉴黄操作,判断图片是否合规。这里直接接入百度API,但是百度接口响应速度有点慢,导致整个图片上传时间得4秒钟左右。接下来我们看下关键代码:
async.waterfall([
function (callback) {
connection.beginTransaction(function (err) {
return callback(err);
});
},
//获取access_token
function (callback) {
postHelper.baseRequest(CONFIG.USERDEFINEPATH, {
grant_type: CONFIG.USERDEFINEGRANTTYPE,
client_id: CONFIG.USERDEFINEKEY,
client_secret: CONFIG.USERDEFINESECRET
}, function(err, result) {
if(err) {
return callback(err);
}
return callback(null, JSON.parse(result).access_token);
});
},
//图像审核
function (access_token, callback) {
postHelper.baseRequestbdai(CONFIG.CENSORINGPATH + access_token, {
imgUrl: data,
imgType: CONFIG.CENSORINGIMGTYPE
}, function(err, result) {
if(err) {
return callback(err);
}
if(result.error_code) {
return callback(result.error_msg);
}
if(result.conclusionType != 1) {
return callback('图片内容不合法,请重新上传吧!');
}
return callback(null, true, result);
});
}
], function (DbErr, isSuccess, uidOrInfo) {
if (DbErr || !isSuccess) {
connection.rollback(function () {
connection.release();
});
return cb(DbErr);
}
connection.commit(function (e) {
if (e) {
connection.rollback(function () {
connection.release();
});
return cb(e);
}
connection.release();
return cb(null, uidOrInfo);
});
});
到这里我们前后端都成功实现,我们就可以来测试下会话系统。我在模拟器发送测试实时聊天系统,然后在手机真机测试看看能不能通过轮询获取:
然后测试发送暴恐图片看看会不会检测出图片不合法:
到这里通过http轮询的方式我们就已经成功实现实时会话系统,但是也正如我们刚才所说的http轮询的缺点,我们一直停留在聊天界面,但是并没有一直处于聊天界面,这样实际上每一次轮询的数据都是旧数据,但是轮询不会停止所以会消耗带宽和服务器资源。所以很明显使用http轮询实现实时会话系统不是不行,但是肯定不是合理的方案,只适用于业务场景较小的应用。下一篇我们开始入门websocket,使用express-ws库改写http轮询实现实时会话系统。
目前整个项目前后端已开源于码云,欢迎来一个star。源码地址:
https://gitee.com/mqzuimeng_admin/wx_blog.git