使用 Vue 实现与融云的即时通信

3,861 阅读4分钟

介绍

因为有同学问了我下融云怎么写聊天的问题,我才了解到有融云这么个东西,尝试着学了下。主要介绍了vue中使用vue-cli接入融云实现即时通信的相关资料,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...

准备

注册融云拿到 APPKEY 和 TOKEN

在写代码之前我们首先要 注册融云,注册融云后拿到 appkey 和融云提供的 token

拿到 appkey 和 token 后记得保存下来

安装 Vue CLI

可以使用下列任一命令安装这个新的包:

$ npm install -g @vue/cli
# OR
$ yarn global add @vue/cli

创建一个项目

运行以下命令来创建一个新项目:

$ vue create hello-world
# OR
$ vue ui  // UI 界面创建项目

你会被提示选取一个 prese 因为我们这个项目用到了 VUEX 所以我们选择手动选择特性来选取需要的特性来避免后续的的麻烦

除了默认选项 我们需要选择 RouterVuex

等项目创建完毕后大概是这个样子

开始编写我们的代码

在这之间还是希望大家去过一遍官方文档后来看我的文章

Web SDK 开发指南

引入融云 Web SDK

首先我们进入根目录的 public 目录下的 index.html 这个位置引入融云 SDK

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>im</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but im doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>

    <script src="https://cdn.ronghub.com/RongIMLib-2.5.0.min.js"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

我们先在根目录 src 下创建几个文件 名字随意大家可以自行创建

  • 创建一个js文件 utils.js
  • 创建 scss 目录 在此目录创建一个 utils.scss (这个文件并不重要只是为了自适应)
  • scr 目录下创建 init.vue
  • src 目录下创建 RongCloud.vue
  • src 目录下创建 Message.vue

开始撸代码

utils.js 此文件是对融云的初始化操作

export const init = (params, addPromptInfo) => {
  var appkey = params.appkey
  var token = params.token
  RongIMClient.init(appkey)
  RongIMClient.setConnectionStatusListener({
    onChanged: function (status) {
      switch (status) {
        case RongIMLib.ConnectionStatus['CONNECTED']:
        case 0:
          addPromptInfo('连接成功')
          break

        case RongIMLib.ConnectionStatus['CONNECTING']:
        case 1:
          addPromptInfo('正在连接中')
          break

        case RongIMLib.ConnectionStatus['DISCONNECTED']:
        case 2:
          addPromptInfo('当前用户主动断开链接')
          break

        case RongIMLib.ConnectionStatus['NETWORK_UNAVAILABLE']:
        case 3:
          addPromptInfo('网络不可用')
          break

        case RongIMLib.ConnectionStatus['CONNECTION_CLOSED']:
        case 4:
          addPromptInfo('未知原因,连接关闭')
          break

        case RongIMLib.ConnectionStatus['KICKED_OFFLINE_BY_OTHER_CLIENT']:
        case 6:
          addPromptInfo('用户账户在其他设备登录,本机会被踢掉线')
          break

        case RongIMLib.ConnectionStatus['DOMAIN_INCORRECT']:
        case 12:
          addPromptInfo('当前运行域名错误,请检查安全域名配置')
          break
      }
    }
  })

  // 消息监听器
  RongIMClient.setOnReceiveMessageListener({
    // 接收到的消息
    onReceived: function (message) {
      // 判断消息类型
      switch (message.messageType) {
        case RongIMClient.MessageType.TextMessage:
          // message.content.content => 文字内容
          addPromptInfo('新消息 ' + message.targetId + ':' + JSON.stringify(message))
          break
        case RongIMClient.MessageType.VoiceMessage:
          // message.content.content => 格式为 AMR 的音频 base64
          break
        case RongIMClient.MessageType.ImageMessage:
          // message.content.content => 图片缩略图 base64
          // message.content.imageUri => 原图 URL
          break
        case RongIMClient.MessageType.LocationMessage:
          // message.content.latiude => 纬度
          // message.content.longitude => 经度
          // message.content.content => 位置图片 base64
          break
        case RongIMClient.MessageType.RichContentMessage:
          // message.content.content => 文本消息内容
          // message.content.imageUri => 图片 base64
          // message.content.url => 原图 URL
          break
        case RongIMClient.MessageType.InformationNotificationMessage:
          // do something
          break
        case RongIMClient.MessageType.ContactNotificationMessage:
          // do something
          break
        case RongIMClient.MessageType.ProfileNotificationMessage:
          // do something
          break
        case RongIMClient.MessageType.CommandNotificationMessage:
          // do something
          break
        case RongIMClient.MessageType.CommandMessage:
          // do something
          break
        case RongIMClient.MessageType.UnknownMessage:
          // do something
          break
        default:
        // do something
      }
    }
  })

  RongIMClient.connect(token, {
    onSuccess: function (userId) {
      addPromptInfo('连接成功,用户id:' + userId, userId)
    },
    onTokenIncorrect: function () {
      addPromptInfo('token无效')
    },
    onError: function (errorCode) {
      switch (errorCode) {
        case RongIMLib.ErrorCode.TIMEOUT:
          addPromptInfo('超时')
          //链接超时进行重新的链接start
          var callback = {
            onSuccess: function (userId) {
              console.log("Reconnect successfully." + userId);
            },
            onTokenIncorrect: function () {
              console.log('token无效');
            },
            onError: function (errorCode) {
              console.log(errorcode);
            }
          };
          var config = {
            // 默认 false, true 启用自动重连,启用则为必选参数
            auto: true,
            // 重试频率 [100, 1000, 3000, 6000, 10000, 18000] 单位为毫秒,可选
            url: 'cdn.ronghub.com/RongIMLib-2.5.0.min.js',
            rate: [100, 1000, 3000, 6000, 10000]
          };
          RongIMClient.reconnect(callback, config);
          //链接超时进行重新链接end
          break;
        case RongIMLib.ErrorCode.UNKNOWN_ERROR:
          addPromptInfo('未知错误')
          break;
        case RongIMLib.ErrorCode.UNACCEPTABLE_PaROTOCOL_VERSION:
          addPromptInfo('不可接受的协议版本')
          break;
        case RongIMLib.ErrorCode.IDENTIFIER_REJECTED:
          console.log('ddd')
          addPromptInfo('appkey不正确')
          break;
        case RongIMLib.ErrorCode.SERVER_UNAVAILABLE:
          addPromptInfo('服务器不可用')
          break;
      }
      addPromptInfo(errorCode)
    }
  }, null)
}

utils.scss 这个文件很简单

@function vw ($px) {
  @return $px / 750px * 100vw;
}

init.vue 通过用户输入的信息来进行初始化连接

<template>
  <div class="init">
    <van-nav-bar title="连接融云" />
    <van-cell-group>
      <van-field v-model="appkey"
                 required
                 label="appkey"
                 placeholder="请输入您的 appkey" />
      <van-field v-model="token"
                 label="token"
                 placeholder="请输入您的 token"
                 required />
      <van-field v-model="targetId"
                 label="targetId"
                 placeholder="请输入 targetId"
                 required />
    </van-cell-group>
    <van-button class="init-button"
                type="info"
                @click="initRongCloud">初始化连接</van-button>
    <div class="rong-show-box">
      <p v-for="data in showDatas"
         v-bind:key="data">
        {{data}}
      </p>
    </div>
  </div>
</template>

<script>
import { init } from '@/utils.js'

export default {
  data () {
    return {
      appkey: '',    // 这是我们之前保存的 appkey *重要
      token: '',     // token 可以多次生成 之前也有介绍过
      targetId: '',  // 你要给谁发送消息 目标ID
      showDatas: [], // 初始化信息
    }
  },
  methods: {
    addPromptInfo (prompt, userId = null) {
      const _this = this

      const avatarList = [
        'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=4100987808,2324741924&fm=26&gp=0.jpg',
        'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2988245209,2476612762&fm=26&gp=0.jpg',
        'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4259300811,497831842&fm=26&gp=0.jpg',
        'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3684587473,1286660191&fm=26&gp=0.jpg',
        'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2884107401,3797902000&fm=26&gp=0.jpg'
      ]
      
      // 真实环境是通过登录 后台接口返回的 token 拿到的用户信息  我在这为为了模拟 所以给初始化后的用户随机生成一个头像
      const avatar = avatarList[Math.floor(Math.random() * (3 + 1))] 
      _this.showDatas.push(prompt)
      const timer = setInterval(() => {
        if (userId) {
          clearInterval(timer) // 路由跳转后销毁定时器
          _this.$store.commit('SET_MEMBER', {   // 保存用户信息
            userId: userId,
            avatar: avatar
          })
          _this.$store.commit('SET_TARGETID', _this.targetId)  // 保存目标ID
          _this.$router.push({ name: 'RongCloud' })
        }
      }, 500)
    },
    initRongCloud () {
      var appkey = this.appkey
      var token = this.token
      if (!appkey || !token) {
        alert('appkey 和 token 不能为空')
      } else {
        // 这个init 是我们之前撸的 `utils.js`
        init({
          appkey: appkey,
          token: token
        }, this.addPromptInfo)
      }
    }
  }
}
</script>

<style lang="scss" scoped>
@import "~@/scss/utils";
.init-button {
  position: fixed !important;
  bottom: vw(30px);
  left: 50%;
  transform: translateX(-50%);
}

.rong-show-box {
  margin-top: vw(100px);
  text-align: center;
}
</style>

RongCloud.vue 发送消息

<template>
  <div id='rongcloud'>
    <van-nav-bar title="融云聊天"
                 fixed
                 left-text="返回"
                 left-arrow
                 @click-left="onClickLeft" />
    <div class="wrapper">
      <Message v-for="(item, index) in answer"
               :key="index"
               :data='item' />
    </div>
    <div class="send-message">
      <van-field v-model="say"
                 class="message-textarea"
                 type="textarea"
                 placeholder="请输入..." />
      <van-button class="send-button"
                  type="info"
                  size="small"
                  @click="sendMessage">发送</van-button>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import Message from './Message'
export default {
  components: {
    Message 
  },
  data () {
    return {
      say: ''  // 消息
    }
  },
  created () {
    this.$nextTick(() => {
      const list = document.getElementById('rongcloud')
      document.documentElement.scrollTop = list.scrollHeight
      //如不行,请尝试-> list.scrollTop = list.scrollHeight
    })
  },
  watch: {
    answer () {
      this.$nextTick(() => {
        const list = document.getElementById('rongcloud')
        document.documentElement.scrollTop = list.scrollHeight
        //如不行,请尝试-> list.scrollTop = list.scrollHeight
      })
    }
  },
  computed: {
    ...mapState({
      answer: 'answer',         // 消息列表
      memberInfo: 'memberInfo', // 用户信息
      targetId: 'targetId'      // 目标ID
    })
  },
  methods: {
    onClickLeft () {
      this.$router.go(-1)
    },
    sendMessage () {
      const _this = this

      var msg = new RongIMLib.TextMessage({ content: _this.say, extra: _this.memberInfo.avatar });
      var conversationType = RongIMLib.ConversationType.PRIVATE // 单聊, 其他会话选择相应的消息类型即可
      var targetId = this.targetId // 目标 Id
      RongIMClient.getInstance().sendMessage(conversationType, targetId, msg, {
        onSuccess: function (message) {
          // message 为发送的消息对象并且包含服务器返回的消息唯一 Id 和发送消息时间戳
          const say = {
            css: 'right',
            txt: message.content.content,
            headImg: _this.memberInfo.avatar
          }
          _this.answer.push(say)
          _this.say = ''
        },
        onError: function (errorCode, message) {
          var info = ''
          switch (errorCode) {
            case RongIMLib.ErrorCode.TIMEOUT:
              info = '超时'
              break
            case RongIMLib.ErrorCode.UNKNOWN:
              info = '未知错误'
              break
            case RongIMLib.ErrorCode.REJECTED_BY_BLACKLIST:
              info = '在黑名单中,无法向对方发送消息'
              break
            case RongIMLib.ErrorCode.NOT_IN_DISCUSSION:
              info = '不在讨论组中'
              break
            case RongIMLib.ErrorCode.NOT_IN_GROUP:
              info = '不在群组中'
              break
            case RongIMLib.ErrorCode.NOT_IN_CHATROOM:
              info = '不在聊天室中'
              break
          }
          console.log('发送失败: ' + info + errorCode)
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
@import "~@/scss/utils";
.wrapper {
  padding-top: vw(92px);
  padding-bottom: vw(200px);
}
.send-message {
  width: 100vw;
  height: vw(200px);
  position: fixed !important;
  bottom: 0;
  left: 0;
  .message-textarea {
    height: 100%;
  }
  .send-button {
    position: fixed;
    right: vw(30px);
    bottom: vw(30px);
  }
}
</style>

Message.vue 显示消息 逻辑比较简单就不讲了

<template>
  <div>
    <div v-if="data.css === 'left'">
      <div class="message left">
        <van-image round
                   fit="cover"
                   width="2rem"
                   height="2rem"
                   :src="data.headImg" />
        <span>{{data.txt}}</span>
      </div>
    </div>
    <div v-if="data.css === 'right'">
      <div class="message right">
        <span>{{data.txt}}</span>
        <van-image round
                   fit="cover"
                   width="2rem"
                   height="2rem"
                   :src="data.headImg" />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ['data']
}
</script>

<style lang="scss" scoped>
@import "~@/scss/utils";
.message {
  display: flex;
  align-items: center;
  padding: vw(10px);
}

.left {
  justify-content: flex-start;
  span {
    margin-left: vw(20px);
  }
}

.right {
  justify-content: flex-end;
  span {
    margin-right: vw(20px);
  }
}
</style>

store.js 此demo的一些状态管理数据

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  memberInfo: undefined,  // 用户信息
  targetId: undefined,    // 目标ID
  answer: []              // 消息列表
}
const mutations = {
  SET_MEMBER (state, memberInfo) {
    state.memberInfo = memberInfo
  },
  SET_TARGETID (state, targetId) {
    state.targetId = targetId
  },
  SET_ANSWER (state, playload) {
    let say = {
      css: 'left',            // css 样式
      txt: playload.content,  // 文本内容
      headImg: playload.extra // 头像
    }
    state.answer.push(say)
  },
};

export default new Vuex.Store({
  state,
  mutations,
  actions
})

最后一步我们需要在 utils.js 文件中引入store 保存发送的消息到 answer

import store from './store'

export const init = (params, addPromptInfo) => {
 ...
  // 消息监听器
  RongIMClient.setOnReceiveMessageListener({
    // 接收到的消息
    onReceived: function (message) {
      // 判断消息类型
      switch (message.messageType) {
        case RongIMClient.MessageType.TextMessage:
          // message.content.content => 文字内容
          store.commit('SET_ANSWER', message.content)
        ...
      }
    }
  })
  ...
}

结尾

本 demo 是给大家实现一个基本思路,只是实现了一个一对一聊天的功能。如需更多功能,请自行深入了解,谢谢。