源起

今年寒假的前半段时间, 在家捣鼓了一个情侣类web应用, 基于aspnetcore和angular搭建的; 寒假中实现了'告白', '相册', '说说', '纪念日'这些功能, 然后前端界面上留一个功能的坑位: 聊天, 点击这个聊天按钮, 可以看到四个字, 那就是敬请期待; 部署上线后, 用户当然只有我和我的"好朋友"使用, "好朋友"先跨了我真棒, 然后问聊天功能马上可以用了吧? 我沉默了, 心想着这个功能后面用signalr试试看吧; 现在已经2020秋了, 聊天功能的界面上依旧是那四个字: 敬请期待

这个国庆, 我意识到不能再拖了, 自己埋的坑, 应该趁早把它填了, 否则"好朋友"会觉得你很菜, 一个"简单的"聊天功能都做不出来;

遇见声网(agora)

最开始想用signalr自己实现聊天功能的, 但是考虑到一方面, 自己的服务器资源有限(1核1G轻量应用服务器); 另一方面, 自身精力能力有限, 写出来也许不难, 但是要写好确是不简单的; 于是寻思着找找现成的东西用用吧, 机缘巧合, 我听说了声网(agora), 于是去他的官网看了一番, 看到有详细的文档, 足量的免费额度...于是决定先白嫖试用一下

关于agora

特地找了一下agora的相关资料, 看起来是挺靠谱的, 在全球都有数据中心和服务器; 小米、陌陌、新东方等知名企业都用过他们的云服务;

基于agora的rtm sdk给我的应用加上聊天功能

参考官网的文档

我的环境

  • win10系统
  • npm包管理
  • angular8.x
  • vscode

步骤

安装依赖

npm i agora-rtm-sdk
安装完后需要修改下agora-rtm-sdk/index.d.ts的文件的2258
原来的内容为:

export type { RtmChannel, RtmClient, RtmEvents, RtmMessage, RtmStatusCode };

修改为:

export { RtmChannel, RtmClient, RtmEvents, RtmMessage, RtmStatusCode };

不修改的话, 编译会报错

引入依赖

因为是在在angular组件ChatComponent中实现聊天相关的功能, 所以在其中引入rtm sdk的依赖 import AgoraRTM from 'agora-rtm-sdk';

创建rtm客户端并登陆到agora的rtm服务器

一行代码创建rtm客户端:

const rtmClient = AgoraRTM.createInstance('<your app id>');
登陆到rtm服务器
const rtmClient = AgoraRTM.createInstance('fd033b52ca5d40599efc96f6e2131639');

async function rtmClientLogin(user: User) {
try {
await rtmClient.login({ token: null, uid: user.id });
} catch(err) {
console.log('AgoraRTM client login failure', err);
}
}

// 在组件的 ngOnInit 方法中调用 rtmClientLogin
async ngOnInit() {
try {
  let user = await this.userServ.getUser().toPromise();
  if (user instanceof User) {
    this.user = user;
    rtmClientLogin(this.user);
  } else {
    throw new Error('无法获取用户数据');
  }
} catch(err) {
  this.notifyServ.error('初始化聊天组件失败', null);
  console.error('初始化聊天组件失败', err);
}
}

ps: 测试阶段, 所以使用的rtm的授权方式是AppID, 如果要使用这种授权方式, 在rtm控制台创建项目的时候要注意一下, 身份认证模式勾选 App ID, 否则在登陆到rtm服务器的时候, 会报红

发送/接收消息

消息发送失败需要通知用户, 错误通知直接使用了antdesign的NzNotificationService, 在构造函数注入即可; 这个应用中, 互相发消息的双方是情侣, User表示当前用户, User.Spouse表示用户的伴侣; 消息发送成功需要清空发送消息文字框并将发送的消息加入消息数组中, 让angular更新视图

发送消息
async sendMessage() {
if (!this.newMessage) {
return;
}
const spouseId = this.user.spouse.id;

try {
const result = await rtmClient.sendMessageToPeer({
  text: this.newMessage
}, spouseId);
if (!result.hasPeerReceived) {
  throw new Error('对方未接受消息');
} else {
  this.messages.push({
    text: this.newMessage,
    sender: this.user,
    receiver: this.user.spouse,
    dateSended: new Date()
  });
  this.newMessage = undefined;
}
} catch (err) {
this.notifyServ.error('发送消息失败', null);
console.log('发送消息失败', err);
}
}

ngOnInit生命周期函数中监听收到新消息事件, 收到新消息后, 将新消息加入消息数组中, angular会通过数据绑定更新视图, 渲染ui

监听并处理收到新消息事件
async ngOnInit() {
try {
let user = await this.userServ.getUser().toPromise();
if (user instanceof User) {
  this.user = user;
  rtmClientLogin(this.user);
  监听接收到消息事件
  rtmClient.on('MessageFromPeer', (rtmMessage, peerId) => {
    this.messages.push({
      text: rtmMessage.text,
      sender: this.user.spouse,
      receiver: this.user,
      dateSended: new Date()
    });
  });
  
} else {
  throw new Error('无法获取用户数据');
}
} catch(err) {
this.notifyServ.error('初始化聊天组件失败', null);
console.error('初始化聊天组件失败', err);
}
}

前端html代码
<div id="container">
<div class="messages">
    <div class="message-item"
         *ngFor="let msg of messages">
        <div class="sendedMessage"
             *ngIf="msg.sender.id === user.id">
            <span class="message-text">{{msg.text}}</span>
            <span>
                <nz-avatar nzIcon="user"
                           [nzSrc]="msg.receiver.profileImageUrl"></nz-avatar>
            </span>
        </div>

        <div class="receviedMessage"
             *ngIf="msg.sender.id === user.spouse.id">
            <span>
                <nz-avatar nzIcon="user"
                           [nzSrc]="msg.sender.profileImageUrl"></nz-avatar>
            </span>
            <span class="message-text">{{msg.text}}</span>
        </div>
    </div>
</div>

<div class="new-message">
    <div nz-row
         nzJustify="end">
        <div nz-col
             nzSpan="18">
            <textarea nz-input
                      [(ngModel)]="newMessage"
                      [nzAutosize]="{ minRows: 1, maxRows: 6 }"></textarea>
        </div>
        <div nz-col
             nzSpan="6">
            <button nz-button
                    nzType="primary"
                    class="mx-auto"
                    style="width: 100%;"
                    (click)="sendMessage()">发送</button>
        </div>
    </div>
</div>
</div>

前端css
:host {
height: 100%;
display: block;
position: relative;
}

#container {
height: 100%;
display: flex;
flex-direction: column;
padding-top: 4px;
}

.messages {
flex-grow: 1;
}

nz-alert {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 85%;
text-align: center;
}

.sendedMessage {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 8px;
}

.receviedMessage {
margin-bottom: 8px;
}

.message-text {
background: #fff;
padding: 8px 4px;
}

.sendedMessage .message-text {
margin-right: 4px;
}

.receviedMessage .message-text {
margin-left: 4px;
}

效果如何?

动图演示:

静图:

小结

上文基于agora的rtm sdk, 初步实现了简单的聊天功能; 体验下来感觉很方便, 不需要关注后端实现, 只需要处理前端逻辑即可轻松构建出实时聊天功能; 当然, 正式在生产环境使用, 还是需要后端配合生成一个身份认证令牌(token)来保证安全性的; 上文暂时只实现了文字的发送接收, 实际上rtm sdk还支持文件和图片的收发, 功能很强大, 有机会再继续探索;