文章目录
- 前言
- 一、用户端
- 1.基本展示
- 2.难处理的点
- 二、另一用户端
- 1.前端websocket的整合
- 2.手机息屏websocket断线问题
- 2.websocket服务端配置
- 3.后端整合websocket作为服务端,传输消息给前端
- 总结
前言
最近工作需求来了个项目,前景为在支付宝平台上发布一个处方插件即小程序插件: 源于我们日常微信聊天的页面,只是旨在重心不同,微信着重于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 {@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(chatHandler(), "/gjws").addInterceptors(new WebSocketHandShakeInterceptor()).setAllowedOrigins("*");}@Beanpublic WebSocketHandler chatHandler() {return new ChatHandler();}@Beanpublic 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作为服务端,传输消息给前端
@Overridepublic 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 blocklogger.error(e.getMessage(), e);response.setHead(ResponseHead.buildFailedHead());response.setData("发送失败");}return response;}
大部分系统是基于微服务的,我这边另一用户端回复消息时,是通过接口回调到我这边的服务,我这边将消息封装好ImWebsocketClient wc = new ImWebsocketClient(new URI(url));
进行建立连接。建立连接成功后,触发TextWebSocketHandler的afterConnectionEstablished方法
@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// TODO Auto-generated method stubsuper.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的确认从而找到对应的对象去发送。
@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// TODO Auto-generated method stubChatMessageBean 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框架便可。如果有后续下咨询的朋友,请在下文评论区留言。