支付宝小程序平台的IM聊天插件


前言

最近工作需求来了个项目,前景为在支付宝平台上发布一个处方插件即小程序插件: 源于我们日常微信聊天的页面,只是旨在重心不同,微信着重于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框架便可。如果有后续下咨询的朋友,请在下文评论区留言。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值