记录一下后台使用 SignaIR, 前台使用 Vue 实现一对一的聊天功能

# 效果展示

# SignaIR 后端实现

后端的环境是 asp.net core。版本是.net5,ORM 是 EFCore

# 实现思路

我本地 SignaIR 的实现是基于之前自己写的后端管理 demo. 已经有了用户表以及 jwt 的令牌授权认证。
所以会将每次客户端连接 SignaIR 后端服务的 ConnectionId 保存在对应的用户表字段里。来达到 SignaIR 的
ConnectionId 与登录用户对应

# 创建一个 ChatHub 类

[Route ("/chatHub")]
    public class ChatHub:Hub
    {
        private readonly ChatMessageController _chatMessage;
        private readonly IHttpContextAccessor _httpContextAccessor;
        public ChatHub (ChatMessageController chatMessage, IHttpContextAccessor httpContextAccessor) 
        {
            _chatMessage = chatMessage;
            _httpContextAccessor = httpContextAccessor;
        }
        public async Task SendMessage (int userId, int targetUserId, string message)
        {
            // 找到发送消息的人员
            List<string> connectionIds = _chatMessage.GetConnectionIds (userId, targetUserId).Where (x=>!string.IsNullOrWhiteSpace (x)).ToLis ();
            // 发送消息的模型
            var sendMessages = _chatMessage.SendMessage (userId, targetUserId, message);
           // 发送消息给对应的客户端 (用户)
            await Clients.Clients (connectionIds).SendAsync ("ReceiveMessage", sendMessages);
        }
        /// <summary>
        /// 当有新的客户端会触发此方法
        /// </summary>
        /// <returns></returns>
        public override async Task OnConnectedAsync ()
        {
            var accessToken = _httpContextAccessor.HttpContext.Request.Query ["access_token"];
            // 应用的项目已经使用了 jwt 令牌。vue 前端要传 header。后续的前端实现会写到
            var userName = _httpContextAccessor.HttpContext.User.Claims.Where (x => x.Type == "userName").FirstOrDefault ()?.Value;
            // 将登陆的用户 connectionId 更新到数据库
            await _chatMessage.UpdateSignaIRConnectionId (userName, Context.ConnectionId);
            // 重新连接初始化聊天历史记录 (现在没用到,前端会调用另外的接口初始化历史记录)
            var chatMessages = _chatMessage.InitMessage (userName);
            //await Clients.Client (Context.ConnectionId).SendAsync ("ReceiveMessage", chatMessages);
        }
    }
    public class SendMessage 
    {
        public DateTime Date { get; set; }
        public string Message { get; set; }
        public int SendUserId { get; set; }
        public int RecvieUserId { get; set; }
        public int UserId { get; set; }
        public string Name { get; set; }
        public string Avatar { get; set; }
    }

# 数据库发送消息的实体

public  class ChatMessage:Entity
    {
        [Key]
        public int Id { get; set; }
        /// <summary>
        /// 发送者用户 Id
        /// </summary>
        public int SendUserId { get; set; }
        /// <summary>
        /// 接受者用户 Id
        /// </summary>
        public int ReceiveUserId { get; set; }
        /// <summary>
        /// 该消息拥有者
        /// </summary>
        public int UserId { get; set; }
        /// <summary>
        /// 发送的消息
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// 消息阅读状态 0 未阅读,1 已阅读
        /// </summary>
        public int ReadState { get; set; }
        /// <summary>
        /// 消息位置 1 右侧,2 左侧
        /// </summary>
        public int Side { get; set; }
    }

# 数据库消息记录

# 后端配置

public void ConfigureServices (IServiceCollection services)
 {
   services.AddAuthentication (JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer (options =>
    {
      options.Events = new JwtBearerEvents
      {
          // 添加接收消息时的事件
          OnMessageReceived = context =>
            {
              var accessToken = context.Request.Query ["access_token"];
              var path = context.HttpContext.Request.Path;
             if (!string.IsNullOrWhiteSpace (accessToken))// 这里可以修改为你相应的 hub 地址
              {
                 context.Token = accessToken;
               }
             return Task.CompletedTask;
         },
     };
    })
        //SignalR
        services.AddSignalR ();
        // 注入聊天
        services.AddScoped<ChatMessageController>();
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
 }
    public void Configure (IApplicationBuilder app, IWebHostEnvironment env)
    {
            app.UseEndpoints (endpoints =>
            {
                endpoints.MapControllers ();
                //signaIR (对应创建的 chatHub 类)
                endpoints.MapHub<ChatHub>("/chatHub");
            });
    }

# Vue 前端实现

# 前端的聊天显示组件是 JwChat

组件具体用法 JwChat

# 安装和使用 SignaIR

  • npm 安装
npm install @microsoft/signalr
  • 引入 SignaIR
const signalR = require("@microsoft/signalr");

# 前端代码实现 (对 JwChat 样式进行了一些修改)

<template>
  <div>
    <JwChat-index
      ref="jwChat"
      :taleList="list"
      v-model="inputMsg"
      @enter="bindEnter"
      height="630px"
      width="100%"
      :scrollType="scrollType"
      :config="config"
      :winBarConfig="winBarConfig"
      :showRightBox="true"
    >
    </JwChat-index>
  </div>
</template>
<script>
const signalR = require ("@microsoft/signalr");
import { method, fetch } from "../../network/request.js";
const listData = [
  {
    //date: "",
    //text: { text: "起床不" },
    //mine: false,
    //name: "woris",
    //img: "http://106.75.244.75:82/upload/2022-08-17/20220817023058.png",
  },
];
function getListArr (size) {
  const listSize = listData.length;
  if (!size) {
    size = listSize;
  }
  let result = [];
  for (let i = 0; i < size; i++) {
    const item = listData [i]; //[Math.random ()*listSize>>0]
    item.id = Math.random ().toString (16).substr (-6);
    result.push (item);
  }
  return result;
}
export default {
  data () {
    return {
      inputMsg: "",
      list: [],
      connection: "",
      scrollType: "scroll",
      _state: this.state,
      friendId: 0,
      config: {
        img: "",
        name: "好友聊天",
        dept: "",
      },
      winBarConfig: {
        active: "win01",
        width: "160px",
        listHeight: "60px",
        list: [{}],
        callback: this.bindWinBar,
      },
    };
  },
  props: {
    state: {
      type: Boolean,
      default: false,
    },
  },
  watch: {
    state: {
      immediate: true,
      handler () {
        //this.$nextTick (() => {
        //   this.$refs.jwChat.$refs.chatList.scrollBottom ();
        // });
      },
    },
  },
  methods: {
    // 发送消息
    bindEnter (e) {
      //this.$refs.jwChat.$refs.chatList.scrollBottom ();
      if (this.friendId==0){
         this.$Message.error ("请选择好友后发送")
         return
      }
      const msg = this.inputMsg;
      if (!msg) return;
      // 模拟两个人聊天
      let UserId = this.$store.state.user.id;
      let targetUserId = this.friendId;
      this.connection
        .invoke ("SendMessage", UserId, targetUserId, msg)
        .catch (function (err) {
          return console.error (err.toString ());
        });
    },
    // 收到消息
    ReceiveMessage (messages) {
      //this.$refs.jwChat.$refs.chatList.scrollBottom ();
      let UserId = this.$store.state.user.id;
      messages.forEach ((message) => {
        // 不加后面的判断同时发会消息错乱
        if (UserId == message.userId && ((this.friendId==message.sendUserId && UserId==message.recvieUserId) ||(this.friendId==message.recvieUserId && UserId==message.sendUserId) )) {
          const msgObj = {
            date: message.date,
            text: { text: message.message },
            mine: message.sendUserId == UserId,
            name: message.name,
            img: message.avatar,
          };
          this.list.push (msgObj);
        }
      });
    },
    // 初始化服务器连接
    initSignaIRConnection () {
      let token = this.$store.state.accessToken;
      // 连接 signaIR
      this.connection = new signalR.HubConnectionBuilder ()
        .withAutomaticReconnect () // 断开重新连接
        .withUrl (
          "https://localhost:5001/chatHub",
          // {
          //   skipNegotiation: true,
          //   transport: signalR.HttpTransportType.WebSockets,
          // }
          { accessTokenFactory: () => token } // 带上令牌
        )
        .build ();
      this.connection
        .start ()
        .then (() => {
          this.$Message.success ("连接聊天成功");
        })
        .catch ((e) => {
          this.$Message.error ("连接聊天失败");
        });
      // 注册方法
      this.connection.on ("ReceiveMessage", this.ReceiveMessage);
      //this.connection.on ("NewConnection", this.NewConnection);
    },
    // 初始化好友
    async initFriends () {
      let { code, data } = await fetch (
        method.GET,
        "https://localhost:5001/frineds",
        {
          userName: this.$store.state.user.name,
        }
      );
      this.winBarConfig.list = data.map ((item) => {
        return {
          id: item.id,
          img: item.avatar,
          name: item.name,
          dept: "",
          readNum: 0,
        };
      });
    },
    // 好友聊天记录
    async frinedsChatMessages (friendId) {
      let UserId = this.$store.state.user.id;
      let { code, data } = await fetch (
        method.GET,
        "https://localhost:5001/frinedsChatMessage",
        {
          friendId: friendId,
          owerId: this.$store.state.user.id,
        }
      );
      this.list = data.map ((message) => {
        return {
          date: message.date,
          text: { text: message.message },
          mine: message.sendUserId == UserId,
          name: message.name,
          img: message.avatar,
        };
      });
    },
    async bindWinBar (play = {}) {
      const { type, data = {} } = play;
      console.log (play);
      if (type === "winBar") {
        const { id, dept, name, img } = data;
        this.config = { ...this.config, id, dept, name, img };
        this.winBarConfig.active = id;
        // 好友
        this.friendId = id;
        // 加载化聊天记录
        await this.frinedsChatMessages (id);
        //this.list = getListArr ();
      }
    },
    DomInit () {
      var chat = document.querySelector (".chatPage .web__main");
      //  console.info (  window.getComputedStyle (chat,null)["transform"]);
      //chat.hight = "0px";
      console.info (chat);
    },
  },
  async created () {
    this.$nextTick (async () => {
      await this.initFriends ();
    });
    this.initSignaIRConnection ();
  },
  mounted () {
    //this.list = getListArr ();
    this.$nextTick (() => {
      //this.$refs.jwChat.scrollBottom ();
      //console.info (this.$refs.jwChat.$refs.chatList);
      //this.$refs.jwChat.$refs.chatList.loadDone ();
    });
  },
};
</script>
<style lang="scss">
.ChatPage {
  margin: 0px !important;
  display: flex !important;
  .header {
    display: none !important;
  }
  .rightBox {
    display: none !important;
  }
  .winBar {
    position: relative !important;
    transform: translateX (0px) !important;
  }
  .main {
    display: block !important;
    width: 100% !important;
    height: 100% !important;
  }
  .chatPage {
    height: 100% !important;
  }
  .chatBox {
    height: 100% !important;
  }
  .scroller {
    //border: 1px solid green;
    //overflow: scroll !important;
    .bscroll-indicator {
      display: none;
    }
  }
  .scroller::-webkit-scrollbar {
    width: 0 !important;
  }
  .taleBox {
    //overflow: scroll !important;
    .wrapper::-webkit-scrollbar {
      /* 滚动条整体样式 */
      width: 10px;
      /* 高宽分别对应横竖滚动条的尺寸 */
      height: 1px;
    }
    .wrapper::-webkit-scrollbar-thumb {
      /* 滚动条里面小方块 */
      border-radius: 0px 10px 10px 0px;
      background-color: #b176c5;
    }
    .wrapper::-webkit-scrollbar-track {
      /* 滚动条里面轨道 */
      -webkit-box-shadow: inset 0 0 5px rgba (0, 0, 0, 0);
      background: rgba (255, 255, 255, 0.4);
      border-radius: 0px 10px 10px 0px;
    }
    /* 兼容 IE*/
    .wrapper {
      -ms-scroll-chaining: chained;
      -ms-overflow-style: none;
      -ms-content-zooming: zoom;
      -ms-scroll-rails: none;
      -ms-content-zoom-limit-min: 100%;
      -ms-content-zoom-limit-max: 500%;
      -ms-scroll-snap-type: proximity;
      -ms-scroll-snap-points-x: snapList (100%, 200%, 300%, 400%, 500%);
      -ms-overflow-style: none;
      overflow: auto;
      border: 1px solid #b176c5;
      width: 100% !important;
      height: 100% !important;
    }
    //.wrapper {
    //   border: 1px solid green;
    //   overflow: scroll !important;
    //   overflow-y: none;
    // }
  }
  .toolBox {
    border: 1px dashed #b176c5;
    border-top: none;
  }
  //.web__main
  // {
  //  transition-timing-function: cubic-bezier (0.165, 0.84, 0.44, 1) !important;
  //   transition-property: transform !important;
  //   transition-duration: 0ms !important;
  //   transform: translateX (0px) translateY (-1591px) translateZ (1px) !important;
  // }
}
//.ivu-drawer-body {
//   overflow: scroll !important;
// }
</style>
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

teapus 微信支付

微信支付

teapus 支付宝

支付宝