vue3.0+websocket 实现聊天室功能

<template>
  <div class="container" ref="chatWrapper">
    <div class="header">
      <head-bar class="chat-head" :title="'Chat'" @goback="goBack"></head-bar>
    </div>
    <div class="content" ref="chatContainer">
      <lottieLoad v-if="loadingMore"></lottieLoad>
      <div
        class="chat-item"
        :id="`chat-item-${index}`"
        v-for="(item, index) in chatList"
        :key="index"
        :style="{ flexDirection: item.sender === 'me' ? 'row-reverse' : 'row' }"
      >
        <div class="message-avatar">
          <img v-if="item.from_client_avatar" :src="item.from_client_avatar" />
          <img v-else src="@/assets/images/userProfile.png" alt="" />
        </div>
        <div class="message-content">
          <div
            class="message-header"
            :style="{ textAlign: item.sender === 'me' ? 'right' : 'left' }"
          >
            <span class="message-username">{{ item.from_client_name }}</span>
            <span class="message-time">
              {{ item.time ? item.time.split(' ')[1].substr(0, 5) : '' }}
            </span>
          </div>
          <div
            class="message-text"
            v-html="item.content"
            v-if="!item.is_img"
            :style="{ float: item.sender === 'me' ? 'right' : 'left' }"
          ></div>
          <div
            v-else
            class="message-text massage-img"
            @click="showMoreImg(item, index)"
            :style="{ float: item.sender === 'me' ? 'right' : 'left' }"
          >
            <!-- <van-image :src="item.content">
               @click="showMoreImg(item, index)"
              <template v-slot:loading>
                <van-loading type="spinner" size="20" />
              </template>
            </van-image> -->

            <img v-if="item.content" :src="item.content" alt="" />
          </div>
        </div>
      </div>
      <div class="toBottom" @click="scrollToBottom1" v-if="newMessageNum > 0">
        <img src="@/assets/icons/new-message.png" alt="" />
        <span class="msg-num">{{ newMessageNum }}</span>
        <span>See the latest news</span>
      </div>
    </div>
    <!-- 聊天输入 -->
    <div class="footer">
      <van-row gutter="10">
        <van-col span="18">
          <div class="searchInput" ref="mesageInput">
            <V3Emoji
              size="small"
              :textAreaOption="{ placeholder: 'Your Message' }"
              :custom-theme="customTheme"
              :textArea="true"
              ref="emoji"
              v-model="chatContent"
              class="emoji"
            >
              <img
                class="publicIcon photoImg"
                src="@/assets/icons/photoAlbum.png"
                alt=""
                @click.stop="handlePhoto"
              />
              <img class="publicIcon" src="@/assets/icons/emote.png" alt="" />
            </V3Emoji>
          </div>
        </van-col>
        <van-col span="6">
          <van-button
            style="height: 50px"
            round
            block
            type="primary"
            native-type="submit"
            @click="sendMessage"
          >
            Send
            <!-- {{ t('ey.send') }} -->
          </van-button>
        </van-col>
      </van-row>

      <!-- 图片 -->
      <input
        type="file"
        ref="fileInput"
        style="display: none"
        accept="image/*"
        @change="handleFileChange"
      />
    </div>

  </div>
</template>

js 部分。

<script lang="ts">
import {
  defineComponent,
  ref,
  onMounted,
  toRefs,
  nextTick,
  watch,
  onUpdated,
  computed,
} from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import headBar from '@/components/model/headModel.vue'
import V3Emoji from 'vue3-emoji'
import 'vue3-emoji/dist/style.css'
import websocket from '@/utils/websocket'
import { getChatRoomBind, uploadImg, getHistoryRecord } from '@/api/chatApi'
import { useI18n } from 'vue-i18n'
import lottieLoad from '@/components/model/lottieLoad.vue'
import $okToast from '@/views/mobile/compontents/tipsDialog'
import { load } from '@/views/mobile/compontents/lodding/index'
export default defineComponent({
  name: 'userInfo',
  components: {
    headBar,
    friendRequest,
    V3Emoji,
    lottieLoad,
  },
  setup() {
    const { t } = useI18n()
    const store = useStore()
    const router = useRouter()
    const chatList: any = ref([])
    const isLogin = ref<boolean>(false)
    let ws
    const state = reactive({
      friendShow: false,
    })
    watch(
      () => store.state.chat.client_id,
      (id) => {
        client_id.value = id
      }
    )
    const loadingMore = ref(false)
    onMounted(async () => {
      await getHistory()
      const element: any = chatContainer.value
      if (element) {
        chatList.value.unshift(...sliceHistoryList.value.reverse())
        await nextTick()
        if (chatContainer.value) {
          scrollToBottom()
        }

        element.addEventListener('scroll', (e) => {
          let scrollH = element.scrollHeight
          let clientH = element.clientHeight
          let scrollT = element.scrollTop
          if (clientH + scrollT >= scrollH) {
            newMessageNum.value = 0
          }
          if (scrollT <= 0) {
            if (
              sliceNum.value <= historyList.value.length &&
              !loadingMore.value
            ) {
              sliceNum.value += 10
            }
            loadingMore.value = true
            if (sliceHistoryList.value.length > 0) {
              setTimeout(async () => {
                chatList.value.unshift(...sliceHistoryList.value.reverse())
                await nextTick()
                let firstEle = element.querySelector(
                  `#chat-item-${sliceHistoryList.value.length}`
                )
                element.scrollTo({
                  top: firstEle.offsetTop - 42,
                  behavior: 'instant',
                })
                loadingMore.value = false
              }, 1000)
            } else {
              setTimeout(() => {
                loadingMore.value = false
                // message.warning('no more')
              }, 500)
            }
          }
        })
      }
    })
    const sliceNum = ref(0)
    const newMessageNum = ref(0)
    const client_id = ref()
    const isBind = ref(false)

    const chatContainer = ref()
    //监听信息
    const handleMessage = () => {
      ws.onMessage((data) => {
        let msg = JSON.parse(data)
        if (msg.type == 'connect') {
          client_id.value = msg.client_id
          // store.commit('setClientId', msg.client_id)
          if (isLogin.value) {
            getChatRoomBind(client_id.value).then((res: any) => {
              if (res.code == 200) {
                isBind.value = true
              }
            })
          }
        } else if (msg.type == 'login') {
          $okToast.show({
            type: 'warn',
            content: 'please login',
            textAlign: 'left',
          })
        } else {
          if (msg.from_user_id == store.getters.userInfo.user_id) {
            msg.sender = 'me'
            chatList.value.push(msg)
            newMessageNum.value = 0
            scrollToBottom()
          } else {
            msg.sender = 'other'
            chatList.value.push(msg)
            const element: any = chatContainer.value
            nextTick(() => {
              if (element) {
                const lastElement = element.querySelector(
                  '.chat-item:last-child'
                )
                let observe = new IntersectionObserver((entries) => {
                  //观察该el是否进入可视区域
                  if (entries[0].isIntersecting) {
                    newMessageNum.value = 0
                    scrollToBottom()
                  } else {
                    newMessageNum.value += 1
                  }
                  //停止观察
                  observe.unobserve(lastElement)
                })
                //开始观察
                observe.observe(lastElement)
              }
            })
          }
        }
      })
    }

    watch(
      () => store.getters.token,
      (token) => {
        if (token.access_token) {
          isLogin.value = true
          ws = new websocket('websocket地址')
          handleMessage()
        } else {
          isLogin.value = false
          ws && ws.close(0)
        }
      },
      {
        immediate: true,
      }
    )
    watch(
      () => store.state.dailyDate.chatStatus,
      (flag) => {
        if (flag) {
          scrollToBottom()
        }
      }
    )
    // onUpdated(() => {
    //   if (newMessageNum.value == 0) {
    //     scrollToBottom()
    //   }
    // })
    //滚动到底部
    const scrollToBottom = () => {
      const element: any = chatContainer.value
      const lastElement = element?.querySelector('.chat-item:last-child')
      if (lastElement) {
        setTimeout(() => {
          newMessageNum.value = 0
          lastElement.scrollIntoView({ behavior: 'smooth' })
        }, 0)
      }
    }
    const handleLink = () => {
      router.push({ name: 'Demo' })
    }

    let socket: any = null

    const handleCancle = () => {
      state.friendShow = false
    }

    // 确认
    const handleSubmit = () => {
      state.friendShow = false
    }
    // e, isImg = false
    const sendMessage = () => {
      if (!isLogin.value) {
        $okToast.show({
          type: 'warn',
          content: 'please login',
          textAlign: 'left',
        })
        return
      }
      if (!isBind.value) {
        $okToast.show({
          type: 'warn',
          content: 'please reload',
          textAlign: 'left',
        })
        return
      }
      let msg = {
        chat: 'group',
        type: 'say',
        content: chatContent.value,
        is_img: false,
      }
      ws.send(msg)
      chatContent.value = ''
    }

    //获取历史信息
    const historyList = ref<any[]>([])
    const getHistory = () => {
      let params = {
        limit: 100,
      }
      let ajax = getHistoryRecord(params).then((res) => {
        if (res.code == 200) {
          res.data.forEach((item) => {
            if (item.from_user_id == store.getters.userInfo.user_id) {
              item.sender = 'me'
            }
          })
          historyList.value = res.data
        }
      })
      return ajax
    }

    const sliceHistoryList = computed(() => {
      return historyList.value.slice(sliceNum.value, sliceNum.value + 10)
    })
    //
    const customTheme = {
      'V3Emoji-hoverColor': '#303237',
      'V3Emoji-activeColor': '#303237',
      'V3Emoji-shadowColor': 'none',
      'V3Emoji-backgroundColor': '#222428',
      'V3Emoji-fontColor': '#ffffff',
    }
    const chatContent = ref('')
    const fileInput = ref<HTMLInputElement | null>(null)
    const handlePhoto = () => {
      if (fileInput.value) {
        fileInput.value.click()
      }
    }

    // 相册图片发送
    const handleFileChange = () => {
      if (fileInput.value?.files) {
        // 处理文件
        const file = fileInput.value.files[0]
        if (file.size > 5 * 1024 * 1024) {
          $okToast.show({
            type: 'warn',
            content: 'The picture is too large',
            textAlign: 'left',
          })
          return
        }
        const formData = new FormData()
        formData.append('file', file)
        load.show('Image uploading in progress...')
        uploadImg(formData).then((res) => {
          if (res.code == 200) {
            let url = res.data
            const charContent = {
              chat: 'group',
              type: 'say',
              content: url,
              is_img: true,
            }
            if (fileInput.value) fileInput.value.value = ''
            ws.send(charContent)
            load.hide()
          } else {
            load.hide()
          }
        })
        // 发送formData到服务器 loadingDirective
      }
    }
    const goBack = () => {
      router.go(-1)
    }
    const scrollToBottom1 = () => {
      if (chatContainer.value) {
        chatContainer.value.scrollTop = chatContainer.value.scrollHeight
      }
    }
    const images = ref()
    const imgShow = ref(false)
    const imgIndex = ref(0)
    const showMoreImg = (e, index) => {
      imgShow.value = true
      const newImages: any = []
      chatList.value.map((item, indet) => {
        if (item.is_img) {
          newImages.push({
            img: item.content,
            indexs: indet,
          })
        }
      })
      images.value = newImages

      imgIndex.value = index
      const abs = images.value.findIndex(function (element) {
        return element.indexs === index
      })
      imgIndex.value = abs
    }

    return {
      ...toRefs(state),
      handleFileChange,
      fileInput,
      handlePhoto,
      customTheme,
      chatContent,
      handleLink,
      socket,
      t,
      chatList,
      handleCancle,
      sendMessage,
      scrollToBottom,
      newMessageNum,
      sliceHistoryList,
      chatContainer,
      loadingMore,
      goBack,
      scrollToBottom1,
      images,
      imgIndex,
      handleSubmit,
      showMoreImg,
      imgShow,
    }
  },
})
</script>

3.websocket.ts

import store from '@/store'
import { getChatRoomBind } from '@/api/chatApi'


let heartBeat, //心跳得定时器
    serverHeartBeat, //服务器响应的定时器
    beat_time = 50000, //心跳时间间隔
    reconnectTimer,
    reconnectNum = 3, //重连次数
    reconnectFlag = true, //控制重连,一次一次来
    beat_data = {
        chat:"ping",
  }
let client_id 
let isAccident = 1
export default class WebSocketClient {
  private ws: WebSocket | any;
  private url;
  private message_func;
  constructor(url: string) {
    this.url = url
   this.initWebSocket(url)
   
  }
  initWebSocket(url) {
    this.ws = new WebSocket(url);
    this.ws.onopen = () => {
      //重连之后需要再绑定
      // if (reconnectNum < 3 && store.getters.token.access_token) {
      //   getChatRoomBind(store.state.chat.client_id)
      // }
      // 开始心跳
      console.log('链接成功');
      
      this.heartBeat(1)
      reconnectTimer && clearTimeout(reconnectTimer)
    }; 

    this.ws.onmessage = (event) => {
      
      let msg = JSON.parse(event.data)
      if (msg.chat === 'pong') {
        console.log('正常');
        serverHeartBeat && clearTimeout(serverHeartBeat)
      } 
      if (msg.type == 'connect' && reconnectNum < 3) {
        client_id = msg.client_id
        store.commit('setClientId', msg.client_id)
        if (reconnectNum < 3 && store.getters.token.access_token) {
          getChatRoomBind(client_id)
          reconnectNum = 3
        }
      }
      if (msg.chat !== 'pong') {
        if (this.message_func) {
          this.onMessage(this.message_func)
        }
      }
       
    }  

    this.ws.onclose = () => {
      console.log('断线,onclose');
      
      if (store.getters.token.access_token && isAccident == 1) {
        this.reconnect()
      }
      this.heartBeat(2)
    };

    this.ws.onerror = (error: Event) => {
      if (store.getters.token.access_token) {
        this.reconnect()
      }
      this.heartBeat(2)

    };

  }

  public send(data: any) {

    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error('WebSocket未连接');
    }
  }

  public close(accident = 1) {
    console.log('断线,close');
    this.heartBeat(2)  
    this.ws.close();
    isAccident = accident
    //是否是主动断开的,1意外,0主动
    if (accident == 1) { 
      this.reconnect()
    }
  
  }

  


  // 监听消息
  onMessage(callback: (data: any) => void): void {
    this.ws.onmessage = event => {

      this.message_func = callback
       //忽略心跳返回信息
       let msg = JSON.parse(event.data)
       if (msg.chat !== 'pong') {
         callback(event.data);
      }
      if (msg.chat === 'pong') {
        serverHeartBeat && clearTimeout(serverHeartBeat)
      } 
    }
   
        

     
    
  }
  onOpen(callback: (data:any) => void): void{
    this.ws?.addEventListener('open', (data) => {

      callback(data)
    })
  }


  heartBeat(opa = 1) {
    // 是否开启心跳
    if (opa == 1) {
      heartBeat = setInterval(() => { 
        if (this.ws.readyState === WebSocket.OPEN) {
          console.log('心跳');
          this.send(beat_data)
          serverHeartBeat = setTimeout(() => {
            //3秒内没收到消息,断开重连
            this.close()
            clearInterval(heartBeat)
          }, 3000);
        }  
        //
      }, beat_time)
      
    } else {
      heartBeat && clearInterval(heartBeat)
      serverHeartBeat && clearTimeout(serverHeartBeat)
     }
  }
  
  reconnect() {
    heartBeat && clearInterval(heartBeat)
    serverHeartBeat && clearTimeout(serverHeartBeat)
    if(!reconnectFlag) return
    if (reconnectNum > 0 && reconnectFlag) {
      reconnectTimer = setTimeout(() => {
        console.log('重连',reconnectNum);
        
        this.initWebSocket(this.url)
        reconnectNum -= 1
        reconnectFlag = true
      }, 5000);
      reconnectFlag = false
    } else {
      console.error('websocket error');
    }
  }
  
}

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
以下是一个简单的使用Spring Boot、Vue.js和WebSocket实现聊天室的代码示例: Spring Boot后端代码: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new WebSocketHandler(), "/chat").setAllowedOrigins("*"); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } } class WebSocketHandler extends TextWebSocketHandler { private static final Map<WebSocketSession, String> users = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { users.put(session, "Anonymous"); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { ChatMessage chatMessage = new ObjectMapper().readValue(message.getPayload(), ChatMessage.class); if (chatMessage.getType() == ChatMessage.MessageType.JOIN) { users.put(session, chatMessage.getSender()); } for (WebSocketSession user : users.keySet()) { user.sendMessage(new TextMessage(new ObjectMapper().writeValueAsString(chatMessage))); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { users.remove(session); } } @Data @AllArgsConstructor @NoArgsConstructor class ChatMessage { public enum MessageType { CHAT, JOIN, LEAVE } private String sender; private String content; private MessageType type; public static ChatMessage joinMessage(String sender) { return new ChatMessage(sender, "", MessageType.JOIN); } public static ChatMessage leaveMessage(String sender) { return new ChatMessage(sender, "", MessageType.LEAVE); } } @RestController public class ChatController { @GetMapping("/users") public List<String> users() { return new ArrayList<>(WebSocketHandler.users.values()); } } ``` Vue.js前端代码: ```html <template> <div> <h2>Chat Room</h2> <div> <label>Your name:</label> <input v-model="name" @keyup.enter="join" /> <button @click="join">Join</button> </div> <div v-if="joined"> <div> <label>Message:</label> <input v-model="message" @keyup.enter="send" /> <button @click="send">Send</button> </div> <div> <h3>Users:</h3> <ul> <li v-for="user in users" :key="user">{{ user }}</li> </ul> </div> <div> <h3>Chat:</h3> <ul> <li v-for="chat in chats" :key="chat.id"> <strong>{{ chat.sender }}:</strong> {{ chat.content }} </li> </ul> </div> </div> </div> </template> <script> import SockJS from "sockjs-client"; import Stomp from "stompjs"; export default { data() { return { name: "", message: "", joined: false, chats: [], users: [], stompClient: null, }; }, methods: { join() { const socket = new SockJS("/chat"); this.stompClient = Stomp.over(socket); this.stompClient.connect({}, () => { this.stompClient.subscribe("/topic/chat", (message) => { const chat = JSON.parse(message.body); if (chat.type === "JOIN") { this.users.push(chat.sender); } else if (chat.type === "LEAVE") { this.users.splice(this.users.indexOf(chat.sender), 1); } this.chats.push(chat); }); this.stompClient.send( "/app/chat", JSON.stringify(ChatMessage.joinMessage(this.name)) ); this.joined = true; }); }, send() { this.stompClient.send( "/app/chat", JSON.stringify( new ChatMessage(this.name, this.message, ChatMessage.MessageType.CHAT) ) ); this.message = ""; }, }, }; class ChatMessage { static MessageType = { CHAT: "CHAT", JOIN: "JOIN", LEAVE: "LEAVE", }; constructor(sender, content, type) { this.sender = sender; this.content = content; this.type = type; } static joinMessage(sender) { return new ChatMessage(sender, "", ChatMessage.MessageType.JOIN); } static leaveMessage(sender) { return new ChatMessage(sender, "", ChatMessage.MessageType.LEAVE); } } </script> ``` 在这个示例中,我们使用了Spring Boot的WebSocket支持来处理来自客户端的事件。我们创建了一个WebSocket处理程序,它维护了一个用户会话列表,并在用户加入、离开或发送聊天消息时广播消息到所有连接的客户端。我们还为WebSocket处理程序创建了一个控制器,以便在客户端请求所有当前连接的用户列表时返回它们。 在Vue.js应用程序中,我们使用SockJS和Stomp.js来建立与服务器的WebSocket连接,并处理来自服务器的事件。我们使用Vue.js的数据绑定来更新聊天消息、用户列表和用户输入框中的数据,并在加入聊天室、发送聊天消息或断开连接时发送相关的WebSocket事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值