文章目录
前言
最近工作需求来了个项目,前景为在支付宝平台上发布一个处方插件即小程序插件: 源于我们日常微信聊天的页面,只是旨在重心不同,微信着重于IM日常生活通讯,我的支付宝小程序插件插件重在提供IM问诊功能,以及问诊后,医生进行开方回传给患者用户。
一、用户端
1.基本展示
- 其基本流程概要便是这个输入框回车发送了。
<!-- 文字+图片消息模板-->
<view a:for="{{chatLists}}" a:key="{{index}}" a:for-index="index">
<view class={{item.role=='patient'?"answer":"question"}} id='msg-{{index}}'>
<view class={{item.role=='patient'?"heard_img right" :"heard_img left"}}>
<image mode="scaleToFill" class="user-profile__avatar" src="{{item.userImgSrc}}" />
</view>
<view class={{item.role=='patient'?"answer_text":"question_text"}} data-index="{{index}}" hidden="{{(item.msg_type==='image')}}">
<view class="symbol"></view>
<view class="info">
<text selectable="true">{{item.textMessage}}</text>
</view>
<view a:if={{item.isSending}} class="notice"><image class="icon" mode="scaleToFill" src="{{item.icon}}" /></view>
</view>
<view class={{item.role=='patient'?"answer_text": "question_text"}} hidden="{{!(item.msg_type==='image')}}">
<image mode="aspectFill" onTap="previewImage" data-index="{{index}}" style="width:150px; height:170px" src="{{item.textMessage}}" />
</view>
</view>
</view>
<input a:if="{{inputObj.inputStatus==='text'}}" class="chat-input-style"
selection-start="-1" selection-end="-1" cursor="-1" maxlength="500" confirm-type="send"
value="{{textMessage}}"
adjust-position='{{false}}'
focus="{{focus}}"
onConfirm="chatInputSendTextMessage"
onFocus="chatInputBindFocusEvent"
onBlur="chatInputBindBlurEvent"
onInput="chatInputGetValueEvent"
confirm-hold="{{true}}"
placeholder='想和TA说点什么呢?'
cursor-spacing='20'/>
其核心便是这个input组件的onConfirm属性,通过在js中设置方法回调进行将input的value获取,然后把整个页面的消息chatLists进行setData重新渲染即可。
2.难处理的点
如上基本展示是很容易去实现的,只要把握好页面结构html以及css样式即可。但是如下如果是要在支付宝小程序平台下去仿微信这样基于日常IM聊天的效果的体验感,是有点难度的,因为小程序是基于web开发的。**然后需要优化的点便是1.点击额外功能区把页面顶起来。2.在上滑获得聊天记录时,如果点击输入框,弹出键盘时,需要聚焦回到最底部消息 3.滑动页面时候在ios上会有卡顿的现象(估计是帧率的问题) 4.手机息屏1分钟左右,websocket断开的问题,导致无法进行正常通讯 ** 看如下动态图
- 像这个点击 + 进行把额外操作区顶起来的关键代码如下
<scroll-view scroll-y="{{true}}" onTouchStart="clickCloseExtra" class="speak_box" scroll-top="{{scrollTop}}" onScroll="viewScroll" scroll-into-view="{{toView}}" style="margin-bottom:{{marginBottom}};height:{{chatHeight}}px">
.speak_box{
display: block;
-webkit-overflow-scrolling: touch;
height: 100vh;
padding:10px;
}
_page.chatInputExtraClickEvent = function (e) {
_page.setData({
'inputObj.extraObj.chatInputShowExtra': !_page.data.inputObj.extraObj.chatInputShowExtra,
'marginBottom': !_page.data.inputObj.extraObj.chatInputShowExtra?'2.6rem':'.98rem',
scrollTop: _page.data.scrollHeight,
});
extraButtonClickEvent && extraButtonClickEvent(!_page.data.inputObj.extraObj.chatInputShowExtra);
};
对于第一点:点击+进行弹起,其关键在于将整个scroll-view的高度100%于整个屏幕(关键height: 100vh),后续采用margin-bottom,监听+号是否被点击事件(_page.data.inputObj.extraObj.chatInputShowExtra)
去解决setData该属性顶起的高度
对于第二点:在上滑获得聊天记录时,如果点击输入框。其主要是触发input的聚焦事件
_page.chatInputBindFocusEvent = function (e) {
let messageList = _page.data.chatLists;
_page.setData({
'inputObj.inputType': 'text',
'inputObj.extraObj.chatInputShowExtra': false,
'marginBottom': '.98rem',
'scrollTop': _page.data.scrollHeight-550,
toView: 'msg-' +(messageList.length-1)
});
其关键在于toView值,toview是前面scroll-view容器里的scroll-into-view属性的值,当将它进行setData时,由于前端消息数据变量的chatLists遍历渲染时有对应绑定下表,所以在toView时直接将其定位到最后一个消息的位置。完成下滑效果。
次关键点还有scrollTop,其为是滚动到页面的目标位置的API,这里将’scrollTop’: _page.data.scrollHeight-550这样设置,主要是为了scrollView的聚焦点对应上scrollTop,让后续的+点击可以顶起页面。
对于第三点: 在ios端滑动页面不流畅问题,就很好解决。
viewScroll: function(e){
this.data.scrollTop = e.detail.scrollTop;
this.data.scrollHeight = e.detail.scrollHeight;
// 修复画面上下滑出现微抖动问题
//超过阈值进行历史记录回显
if(e.detail.scrollTop==0){
if(this.time){
clearTimeout(this.time);
}
this.time = setTimeout(()=>{
this.setData({
scrollTop: e.detail.scrollTop,
});
},250)
},
在监听的scrollView滑动函数viewScroll里增加一个防抖(微妙级别的定时器)即可。
二、另一用户端
因为涉及websocket讲解,一并将上面,手机息屏1分钟左右,websocket断开的问题,导致无法进行正常通讯的解决方案提供
1.前端websocket的整合
let _this = this;
// 连接
let url = ws_chat+ '?biz=' + biz + '&uid=' + uid + '&name=' + name;
my.connectSocket({
url: url,
data: {},
header:{
'content-type': 'application/json'
},
success: (res) => {
console.log('WebSocket 连接成功');
socket_state =1;
},
fail: (res) => {
my.showToast({
type: 'none',
content: '无法连接服务器,请刷新...',
duration: 1000,
});
},
complete: () => {
}
});
//接受云医服务器传来的数据
my.onSocketMessage(function(result) {
console.log(result);
let resp = JSON.parse(Base64.baseDecode(result.data));
if (resp.type == 'heartbeat') {
return;
}
let content = resp.content;
if (resp.type == 'text') {
Assistant.sendQuestion(content,null,resp.type,false,null,_this,'doctor');
}else if (resp.type == 'image') {
Assistant.sendQuestion(content, null, resp.type, false, null,_this,'doctor');
}else if (resp.type == 'endconsul') {
Assistant.sendQuestion(content, null, "text", false, function() {
_this.setData({
'inputObj.chatInputShowExtra': false,
})
},_this,'doctor');
}else if(resp.type=='rpconsul'){
Assistant.sendQuestion(content, null, 'text', false,function() {
},_this,'doctor');
}
});
自行看代码便可理解。其是简单的一个websocket整合,可自行看支付宝相关文档介绍。
2.手机息屏websocket断线问题
其源于websocket是基于web的,当手机息屏行,可能是手机后台线程也进行闲置,无法继续为websocket提供服务,无法进行心跳继续发送,所以一分钟左右由于心跳无法继续发送就到导致websocket断开,但是重点便是这个websocket在支付宝小程序平台下只是处于断开状态,并没有关闭掉(有区别于微信小程序)。
所以当时查阅支付宝小程序websocket相关机制,1.如果是在websocket长时间心跳无保持的情况下,断开时触发my.onSocketClose的话,然后函数回调进行把websocket对象关闭掉,等待用户手机唤醒屏幕的时候 触发页面的onshow()方法再将websocket重连。 2.或者是直接在手机息屏的时候 触发onHide()方法时,直接将websocket给关闭。
2.websocket服务端配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler(), "/gjws")
.addInterceptors(new WebSocketHandShakeInterceptor())
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler chatHandler() {
return new ChatHandler();
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
代码中便是你websocket的url上下文,如我的配置url为im.server.url = ws://192.168.0.64:5506/gjws?biz=BIZID&uid=UID
关于nginx上的配置参考如下
location /ws {
proxy_pass http://192.168.0.64:5506/gjws;#代理到上面的地址去,
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Real-IP $remote_addr;
}
3.后端整合websocket作为服务端,传输消息给前端
@Override
public ResponseMsg<String> sendMsg(String token, RequestMsg<ReceiveMsgInVo> in) {
// 医生端消息通过websocket发送到患者端
ResponseMsg<String> response = new ResponseMsg<String>();
String url = imServerUrl.replaceAll("BIZID", in.getData().getBiz()).replaceAll("UID", "0");
String msg = JSON.toJSONString(in.getData());
// base64转码
String sm = new String(Base64Utils.encodeToString(msg.getBytes()));
try {
ImWebsocketClient wc = new ImWebsocketClient(new URI(url));
wc.connect();
while (wc.getReadyState().ordinal() == 0) {
Thread.sleep(200);
}
if (wc.getReadyState().ordinal() == 1) {
logger.info("医生端消息推送成功");
wc.send(sm);
}
wc.close();
response.setHead(ResponseHead.buildSuccessHead());
response.setData("发送成功");
} catch (Exception e) {
// TODO Auto-generated catch block
logger.error(e.getMessage(), e);
response.setHead(ResponseHead.buildFailedHead());
response.setData("发送失败");
}
return response;
}
大部分系统是基于微服务的,我这边另一用户端回复消息时,是通过接口回调到我这边的服务,我这边将消息封装好ImWebsocketClient wc = new ImWebsocketClient(new URI(url));
进行建立连接。建立连接成功后,触发TextWebSocketHandler的afterConnectionEstablished方法
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// TODO Auto-generated method stub
super.afterConnectionEstablished(session);
String biz = (String) session.getAttributes().get("biz");
String uid = (String) session.getAttributes().get("uid");
logger.info("Login UID2:"+uid);
//系统消息连接不处理
if("0".equals(uid)) {
return;
}
ChatMessageBean bean = new ChatMessageBean();
bean.setBiz(biz);
bean.setUid(uid);
ChatHelper.addSession(session);
RoomMate user = new RoomMate();
user.setUid(uid);
user.setSid(session.getId());
ChatHelper.onLine(user);
ChatHelper.joinChatRoom(bean,session.getId());
logger.info("Login UID:"+uid);
}
此时在前端的websocket建立连接的时候会触犯这个方法,然后进行重写将该对象与websocket session绑定。后续handler通过biz找到聊天室,获取到用户session,发送消息传至前端。其websocket消息发送关键在于session的确认从而找到对应的对象去发送。
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// TODO Auto-generated method stub
ChatMessageBean bean = JSON.parseObject(Base64Utils.decodeFromString(message.getPayload()), ChatMessageBean.class);
if (MessageTypeEnum.LOGIN.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doLogin(session, bean);
} else if (MessageTypeEnum.HEARTBEAT.getCode().equals(bean.getType())) {
doHeartBeat(session, bean);
} else if (MessageTypeEnum.TEXT.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doText(session, bean);
} else if (MessageTypeEnum.IMAGE.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doText(session, bean);
} else if (MessageTypeEnum.JOIN.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doJoinChatRoom(session, bean);
} else if (MessageTypeEnum.NEWCONSUL.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doNewConsul(session, bean);
} else if(MessageTypeEnum.ENDCONSUL.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doText(session, bean);
} else if(MessageTypeEnum.RPCONSUL.getCode().equals(bean.getType())) {
logger.info(bean.toString());
doText(session, bean);
}
}
private void doText(WebSocketSession session, ChatMessageBean message) throws IOException {
List<RoomMate> mates = new ArrayList<>();
//保存消息
if(MessageTypeEnum.AUTO.getCode().equals(message.getSource())) {
mates = ChatHelper.getAllRoomMates(message.getBiz(),message.getUid());
}else {
mates = ChatHelper.getOtherRoomMates(message.getBiz(),message.getUid());
}
logger.info("聊天室"+message.getBiz()+"在线用户:"+ JSON.toJSONString(mates));
if(mates.size()>0) {
for (RoomMate p : mates) {
if(p!=null) {
WebSocketSession ws = ChatHelper.getSession(p.getSid());
if(ws!=null && ws.isOpen()) {
ws.sendMessage(new TextMessage(Base64Utils.encodeToString(message.toString().getBytes("UTF-8"))));
}
}
}
}
}
如上图,在前端进行websocket消息发送时,会触发handleTextMessage。后续只需对应将消息内容
ws.sendMessage(new TextMessage(Base64Utils.encodeToString(message.toString().getBytes(“UTF-8”))));通过聊天室另一方的userid,然后服务端websocket将消息推送至前端中即可。
总结
提示:这里对文章进行总结:
以上就是今天要讲的内容,本文仅仅简单介绍了部分IM的一个实现,小型通讯量是没问题的,如果后续说有大量的用户去时刻请求,后端可整合netty框架便可。如果有后续下咨询的朋友,请在下文评论区留言。