65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Weavy 和 KendoReact Conversational UI for React 构建聊天应用程序

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2021年8月23日

CPOL

5分钟阅读

viewsIcon

4440

本教程演示了如何使用 KendoReact UI 框架处理来自 Weavy 并由 Weavy 持久化的数据。

引言

本教程将展示如何在 React 项目中使用 KendoReact Conversational UI,并将 Weavy 作为后端。您还将看到在项目中如何使用其他 KendoReact 控件的示例。

要求

  • 本地或远程 Weavy 实例。请参阅 入门指南,了解如何在您的机器上设置 Weavy。
  • 为了加快速度,您可以跳过设置自己的 Weavy 实例,直接使用托管在 https://showcase.weavycloud.com 上的公开可用实例。
  • Visual Studio Code 或其他您偏好的代码编辑器

开始之前

首先,从 Github 克隆项目 - 它包含了我们将用于本教程的所有代码。

该项目已配置为使用托管在https://showcase.weavycloud.com 上的公开可用 Weavy 实例,并且功能齐全。但是,如果您有兴趣设置自己的本地 Weavy 实例作为后端,您也可以从 Github 克隆本教程中使用的 Weavy 解决方案。这需要一些额外的步骤,这些步骤在 配置 部分有详细说明。

应用程序组成

该应用程序将展示一个使用 KendoReact Conversational UI for React 构建的聊天应用的一些基本功能。它不是一个可用于生产环境的应用程序,应被视为一个关于如何在 KendoReact 项目中利用 Weavy 功能的示例。

以下是我们将在本教程中使用到的功能

  • 身份验证 - 基于 Cookie 的身份验证,为了演示目的,使用以下硬编码的 JWT 令牌之一。
  • 列出对话 - 使用 React Avatar 组件显示用户图片的简单列表。
  • 发送和接收消息 - 使用由 Weavy 支持的 React Conversational UI。

配置

constants.js 文件中,有一个 URL 设置,如果您想更改 Weavy 实例,可以进行修改:API_URL - Weavy 实例的 URL

身份验证

在本教程中,我们将展示如何使用基于 Cookie 的身份验证与 Weavy 集成。我们将通过传递为本次演示创建的四个硬编码用户账户之一来请求身份验证 Cookie。

在应用程序启动时呈现给用户的LoginForm.js 文件中,您可以从预定义的用户中进行选择。每个用户都有一个 JWT 令牌,该令牌将被发送到端点https://showcase.weavycloud.com/client/sign-in。成功通过身份验证后,Cookie 将在响应中返回。

请注意! 此演示应用程序没有持久化状态管理。刷新页面时,您将再次看到登录页面。

fetch(API_URL + '/client/sign-in', {
    method: 'GET',
    credentials: 'include',
    headers: {
        'Accept': 'application/json',
        'Authorization': 'Bearer ' + token
    },
})
    .then(res => res.json())
    .then((user) => {
        login(user);
    });

我们为本教程/演示准备的预定义 JWT 令牌仅适用于 https://showcase.weavycloud.com Weavy 测试站点。

聊天应用程序

使用 Kendo React 和 Weavy 构建的聊天应用外观如下。

列出对话

/components/ConversationList.js 组件中,我们向/api/conversations 端点发出请求以获取用户的所有当前对话。我们只需遍历结果,并使用 Kendo React Avatar 组件来渲染用户的头像。

<Avatar shape='circle' type='image'>
    <img src={`${API_URL}${c.thumb.replace('{options}', '34' )}`} />
</Avatar>

用户对话在组件挂载后被获取。

useEffect(() => {
    fetch(API_URL + '/api/conversations/', {
        method: 'GET',
        credentials: 'include',     
        headers: {
            'Accept': 'application/json'        
        },
    })
        .then(res => res.json())
        .then((conversations) => {            
            setConversations(conversations);            
        });
}, []);

聊天组件

当用户点击一个对话时,Kendo React Conversational UI 组件会被填充。请查看/components/Messages.js 文件。首先,会获取对话,然后检索并渲染对话中的所有消息。

<Chat user={user}
      placeholder="Type a message..."
      messages={messages}
      onMessageSend={addNewMessage} />

useEffect(() => {

    if (!props.conversationid) return;

    fetch(API_URL + '/api/conversations/' + props.conversationid + '/messages', {
        method: 'GET',
        credentials: 'include',     
        headers: {
            'Accept': 'application/json'        
        }
    })
        .then(res => res.json())
        .then((r) => {
            setMessages(r.data.map((m) => {
                return {
                    text: m.text,
                    timestamp: new Date(m.created_at),
                    author: { id: m.created_by.id, name: m.created_by.name }
                }
            }));
        });
}, [props.conversationid]);

Weavy 和实时事件

为了利用 Weavy 的实时功能,我们在项目中添加了 signalR。连接由 ConnectionContext 处理。

请查看connection-context.jsconnection-provider.js 文件。当用户登录后,会调用 connect 方法,通过使用 useContext(ConnectionContext) 钩子,可以在所有 React 组件中访问代理集线器。

通过代理集线器,我们可以响应实时分发的各种事件。您可以在 Server API / Weavy.Core.Events 部分找到可用事件的列表。

export const ConnectionContext = React.createContext(
    {
        connect: () => null,
        proxy: null
    }
);

const ConnectionProvider = (props) => {
    const [proxy, setProxy] = useState(null);

    const connect = () => {
        const connection = hubConnection(API_URL);
        const hubProxy = connection.createHubProxy('rtm');
        hubProxy.on('init', (type, data) => { }); // dummy event to get signalR started...
        setProxy(hubProxy);

        if (connection) {
            connection.start();
        }
    }

    return (
        <ConnectionContext.Provider value=>
            {props.children}
        </ConnectionContext.Provider>
    );
}

我们感兴趣的实时事件是 message-inserted 事件 - 用于更新对话列表,以及 badge 事件 - 用于在未读对话数量变化时更新我们的徽章控件。

const ConversationList = (conversationProps) => {
    ...

    const { proxy } = useContext(ConnectionContext);

    ...

    useEffect(() => {
        if (!proxy) return;
        proxy.on('eventReceived', (type, data) => {
            switch (type) {
                case "message-inserted.weavy":
                    messageReceived(data);
                    break;
                default:
            }
        });
    }, [proxy])
}

Weavy 服务器 SDK

让我们暂时离开 KendoReact 项目,关注支持我们一直在处理的用户界面的 Weavy 安装。如果您正在运行https://showcase.weavycloud.com 上的默认项目,可以忽略本节。

配置

首先,克隆 Weavy Showcase 项目。构建项目并在本地或您选择的位置进行部署。有关设置 Weavy 的帮助,请参阅 入门指南

完成设置向导,然后导航到/manage/clients。添加一个新客户端并复制 client id 和 client secret 的值。同时注意 Weavy 配置运行的 URL。

回到 KendoReact 项目。找到 constants.js 文件,打开它并输入 API_URL。

现在您有了自己的 Weavy 实例,演示 JWT 令牌将不再有效。那些是为showcase.weavycloud.com 站点配置的 Client ID 和 Client Secret。您需要创建新的令牌供开发使用,或者如果您想采取更真实的方式,您应该允许您的用户 against 您的宿主系统进行身份验证,然后动态地为该用户创建一个有效的 JWT 令牌。为了最大的安全性,真实的 JWT 令牌应该是短期的。

REST API

Weavy 是高度可扩展的,使开发人员能够修改产品的绝大部分。对于本项目,我们一直在使用为本项目定制的 REST API 端点。能够修改和扩展 Weavy REST API 可以成为构建高效和复杂应用程序的强大工具。下面是本教程中使用到的端点。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.Http.Description;
using Weavy.Areas.Api.Models;
using Weavy.Core;
using Weavy.Core.Models;
using Weavy.Core.Services;
using Weavy.Web.Api.Controllers;
using Weavy.Web.Api.Models;

namespace Weavy.Areas.Api.Controllers {

    /// <summary>
    /// Api controller for manipulating Conversations.
    /// </summary>
    [RoutePrefix("api")]
    public class ConversationsController : WeavyApiController {

        /// <summary>
        /// Get the <see cref="Conversation" /> with the specified id.
        /// </summary>
        /// <param name="id">The conversation id.</param>
        /// <example>GET /api/conversations/527</example>
        /// <returns>The specified conversation.</returns>
        [HttpGet]
        [ResponseType(typeof(Conversation))]
        [Route("conversations/{id:int}")]
        public IHttpActionResult Get(int id) {
            // read conversation
            ConversationService.SetRead(id, DateTime.UtcNow);

            var conversation = ConversationService.Get(id);

            if (conversation == null) {
                ThrowResponseException(HttpStatusCode.NotFound, $"Conversation with id {id} not found.");
            }
            return Ok(conversation);
        }

        /// <summary>
        /// Get all <see cref="Conversation" /> for the current user.
        /// </summary>        
        /// <example>GET /api/conversations</example>
        /// <returns>The users conversations.</returns>
        [HttpGet]
        [ResponseType(typeof(IEnumerable<Conversation>))]
        [Route("conversations")]
        public IHttpActionResult List() {
            var conversations = ConversationService.Search(new ConversationQuery());
            return Ok(conversations);
        }

        /// <summary>
        /// Create a new or get the existing conversation between the current- and specified user.
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        [ResponseType(typeof(Conversation))]
        [Route("conversations")]
        public IHttpActionResult Create(CreateConversationIn model) {
            string name = null;
            if (model.Members.Count() > 1) {
                name = string.Join(", ", model.Members.Select(u => UserService.Get(u).GetTitle()));
            }

            // create new room or one-on-one conversation or get the existing one
            return Ok(ConversationService.Insert(new Conversation() { Name = name }, model.Members));
        }

        /// <summary>
        /// Get the messages in the specified conversation.
        /// </summary>
        /// <param name="id">The conversation id.</param>
        /// <param name="opts">Query options for paging, sorting etc.</param>
        /// <returns>Returns a conversation.</returns>
        [HttpGet]
        [ResponseType(typeof(ScrollableList<Message>))]
        [Route("conversations/{id:int}/messages")]
        public IHttpActionResult GetMessages(int id, QueryOptions opts) {
            var conversation = ConversationService.Get(id);

            if (conversation == null) {
                ThrowResponseException(HttpStatusCode.NotFound, "Conversation with id " + id + " not found");
            }
            var messages = ConversationService.GetMessages(id, opts);
            messages.Reverse();
            return Ok(new ScrollableList<Message>(messages, Request.RequestUri));
        }

        /// <summary>
        /// Creates a new message in the specified conversation.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        [ResponseType(typeof(Message))]
        [Route("conversations/{id:int}/messages")]
        public IHttpActionResult InsertMessage(int id, InsertMessageIn model) {
            var conversation = ConversationService.Get(id);
            if (conversation == null) {
                ThrowResponseException(HttpStatusCode.NotFound, "Conversation with id " + id + " not found");
            }
            return Ok(MessageService.Insert(new Message { Text = model.Text, }, conversation));
        }

        /// <summary>
        /// Called by current user to indicate that they are typing in a conversation.
        /// </summary>
        /// <param name="id">Id of conversation.</param>
        /// <returns></returns>
        [HttpPost]
        [Route("conversations/{id:int}/typing")]
        public IHttpActionResult StartTyping(int id) {
            var conversation = ConversationService.Get(id);
            // push typing event to other conversation members
            PushService.PushToUsers(PushService.EVENT_TYPING, new { Conversation = id, User = WeavyContext.Current.User, Name = WeavyContext.Current.User.Profile.Name ?? WeavyContext.Current.User.Username }, conversation.MemberIds.Where(x => x != WeavyContext.Current.User.Id));
            return Ok(conversation);
        }

        /// <summary>
        /// Called by current user to indicate that they are no longer typing.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpDelete]
        [Route("conversations/{id:int}/typing")]
        public IHttpActionResult StopTyping(int id) {
            var conversation = ConversationService.Get(id);
            // push typing event to other conversation members
            PushService.PushToUsers("typing-stopped.weavy", new { Conversation = id, User = WeavyContext.Current.User, Name = WeavyContext.Current.User.Profile.Name ?? WeavyContext.Current.User.Username }, conversation.MemberIds.Where(x => x != WeavyContext.Current.User.Id));
            return Ok(conversation);
        }

        /// <summary>
        /// Marks a conversation as read for the current user.
        /// </summary>
        /// <param name="id">Id of the conversation to mark as read.</param>
        /// <returns>The read conversation.</returns>
        [HttpPost]
        [Route("conversations/{id:int}/read")]
        public Conversation Read(int id) {
            ConversationService.SetRead(id, readAt: DateTime.UtcNow);
            return ConversationService.Get(id);
        }

        /// <summary>
        /// Get the number of unread conversations.
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [ResponseType(typeof(int))]
        [Route("conversations/unread")]
        public IHttpActionResult GetUnread() {
            return Ok(ConversationService.GetUnread().Count());
        }
    }
}

在 `StopTyping` 方法中,您可以看到触发自定义事件有多么容易。这些事件可以使用 Weavy Client SDK 轻松挂钩,如 The Weavy Client SDK and Realtime Events 部分所述。

结论

本教程旨在演示如何使用 KendoReact UI 框架处理来自 Weavy 并由 Weavy 持久化的数据。Weavy 是围绕众所周知的标准设计的,并在构建您自己的内容方面提供了极大的灵活性。

© . All rights reserved.