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

使用ASP.NET、ReactJS、Web API、SignalR和Gulp构建的聊天应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (10投票s)

2016年5月16日

CPOL

11分钟阅读

viewsIcon

45098

使用各种技术在ASP.NET环境下构建的一个简单的聊天应用程序

引言

创建复杂的单页Web应用程序从未像现在这样容易。我们有如此多的前端和后端框架可供选择,以至于几乎不可能决定使用哪个来创建我们的精彩应用程序。这就像去购物却无法决定买什么。有时,解决这个问题的最佳方法是选择符合我们最低要求、拥有良好社区支持且易于扩展以实现附加功能的框架和库。还需要考虑其他事项,例如代码库的许可和开发人员的学习曲线。考虑到这一点,我想使用各种技术创建一个简单的聊天应用程序,以展示如何利用Web应用程序开发中不断变化的编程趋势。

此应用程序使用ReactJS、ASP.NET Web Forms、Web API和SignalR创建。此外,还使用了Gulp任务运行器来转译和连接React组件脚本文件。转译步骤是必需的,因为React组件是用jsx语法编写的,我们需要将其转换为与ECMAScript 5兼容的常规JavaScript代码。Web API用于将聊天消息从浏览器发送到服务器,而SignalR则在收到新消息时将其从服务器广播到所有已连接的客户端。我使用SignalR来避免使用Web API进行短轮询。此应用程序可以完全使用SignalR来建立客户端和服务器之间的通信,但我认为展示Web API如何集成到React组件中。

此应用程序允许多个客户端进行群聊。UI是通过React组件创建的。组件使用Web API将消息发送到服务器,SignalR将其广播给所有客户端,以便他们可以接收有关消息和用户的更新信息。

在下一节中,我将解释应用程序的每个组件,并了解所有模块如何相互交互。

必备组件

在开始阅读下一节之前,我假设您对这里使用的所有技术都有相当的了解。ASP.NET代码很容易理解。React新手可能需要一些时间来理解它。

如果您打算在您的应用程序中使用这些代码,请不要将其视为生产就绪代码。我没有实现任何测试用例,也没有进行过重大测试。

Using the Code

创建一个新的ASP.NET Web Forms应用程序。这是一个聊天应用程序,因此最好有一个管理器类来获取和保存聊天消息。我们还需要一个聊天存储对象来存储所有聊天消息和用户列表。单个聊天消息将存储在一个对象中,该对象具有id、消息字符串、时间、用户名和用户id等属性。代码使用Guid类型来管理用户和聊天消息的唯一id。

聊天管理器

namespace ReactChat.App_Start
{
    public class ChatManager
    {
        private ChatStore _chatStore;
        public ChatManager(ChatStore chatStore)
        {
            _chatStore = chatStore;
        }

        public void AddChat(ChatItem chatItem)
        {
            _chatStore.ChatList.Add(chatItem);
        }

        public void AddUser(String userName)
        {
            _chatStore.UserList.Add(userName);
        }

        public List<String> GetAllUsers()
        {
            return _chatStore.UserList;
        }

        public List<ChatItem> GetAllChat()
        {
            return _chatStore.ChatList;
        }
    }
}

聊天管理器接受一个chatstore对象,并使用它来保存新消息和用户。

  • AddChat()将新消息添加到会话中。
  • AddUser()将新用户添加到用户列表中。
  • GetAllUsers()用于返回所有用户名。
  • GetAllChat()返回会话中存储的聊天消息列表。

聊天存储

namespace ReactChat.Models
{
    public class ChatStore
    {
        public List<ChatItem> ChatList { get; set; }
        public List<String> UserList { get; set; }

        public ChatStore()
        {
            ChatList = new List<ChatItem>();
            UserList = new List<String>();
        }
    }
}

聊天存储维护聊天消息和用户的列表。这是一个非常简单的类,具有可以轻松修改的公共列表。

聊天项

namespace ReactChat.Models
{
    public class ChatItem
    {
        public Guid Id { get; set; }
        public Guid UserId { get; set; }
        public String UserName { get; set; }
        public String Message { get; set; }
        public DateTime DateTime { get; set; }
    }
}

聊天项是一个单独的聊天容器,包含与任何聊天消息相关的所有必需信息。

我们需要使用“库包管理器”窗口,在在线搜索SignalR后,添加SignalR依赖项。要使用SignalR,我们需要有一个集线器类。

将以下代码添加到ChatHub.cs

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using ReactChat.Models;

namespace ReactChat.App_Start
{
    [HubName("MyChatHub")]
    public class ChatHub: Hub
    {
        /// <summary>
        /// Broadcasts the chat message to all the clients
        /// </summary>
        /// <param name="chatItem"></param>
        public void SendMessage(ChatItem chatItem)
        {
            IHubContext context = GlobalHost.ConnectionManager.GetHubContext("MyChatHub");
            context.Clients.All.pushNewMessage(chatItem.Id, chatItem.UserId, 
                       chatItem.UserName, chatItem.Message, chatItem.DateTime);
        }

        /// <summary>
        /// Broadcasts the user list to the clients
        /// </summary>
        public void SendUserList(List<String> userList)
        {
            IHubContext context = GlobalHost.ConnectionManager.GetHubContext("MyChatHub");
            context.Clients.All.pushUserList(userList);
        }
    }
}

集线器类将用于发送和接收聊天消息。为此,我们将创建一个新的ChatHub类,该类将继承自Hub类。我们还需要一个HubName属性,以便我们可以在应用程序中的任何位置查找我们的SignalRhub类。

SendMessage()方法用于将聊天消息对象发送到客户端。请注意,pushNewMessage方法名称应与JavaScript函数完全匹配,该函数将在数据发送时在浏览器中调用。

SendUserList()用于将用户列表广播给所有已连接的客户端。

我们还需要添加一个OWIN启动类,将SignalR与应用程序管道进行映射。您可以在以下链接中了解有关OWIN和Katana的更多信息

向项目添加一个新类Startup.cs,并向其中添加以下代码

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(ReactChat.Startup))]

namespace ReactChat
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

Web API控制器

向项目添加一个新的Web API控制器类,并将其命名为ChatController。向此类添加以下代码

聊天控制器

using ReactChat.Models;
using ReactChat.App_Start;

namespace ReactChat.Controllers
{
    public class ChatController : ApiController
    {
        private ChatManager _manager;
        private ChatHub _chatHub;

        public ChatController(ChatManager chatManager)
        {
            _manager = chatManager;
            _chatHub = new ChatHub();
        }

        // GET api/<controller>
        public String GetNewUserId(String userName)
        {
            _manager.AddUser(userName);

            //broadcast the user list to all the clients
            _chatHub.SendUserList(_manager.GetAllUsers());
            return Guid.NewGuid().ToString();
        }

        // GET api/<controller>/5
        public List<ChatItem> Get()
        {
            return _manager.GetAllChat();
        }

        // POST api/<controller>
        public void PostChat(ChatItem chatItem)
        {
            chatItem.Id = Guid.NewGuid();
            chatItem.DateTime = DateTime.Now;
            _manager.AddChat(chatItem);

            //broadcast the chat to all the clients
            _chatHub.SendMessage(chatItem);
        }

        // PUT api/<controller>/5
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/<controller>/5
        public void Delete(int id)
        {
        }
    }
}

我们将使用ASP.NET会话来保存聊天消息。为此,我们需要创建一个新的聊天存储对象并将其添加到会话中。现在,为了使Web API能够将消息保存到会话中,我们需要将聊天管理器对象的依赖注入到Web API控制器的构造函数中。这将使Web API控制器能够使用聊天管理器将传入的消息保存到服务器会话状态。为此,我们需要实现IDependencyResolver接口。这将使我们能够解析并将依赖项注入到Web API控制器类中。在此步骤之后,我们将使用我们新实现的依赖项解析器在全局配置中使用。

在聊天控制器中,我们将有处理传入的GET和POST请求的方法。

  • GetNewUserId将创建一个新的服务器用户,然后将此新创建用户的id返回给客户端。如前所述,我们使用SignalR将信息从服务器发送到客户端。
  • Get()方法将所有聊天消息返回给客户端。
  • PostChat()将新聊天消息添加到集合中。

接下来,添加一个新类并将其命名为WebApiDependencyResolver.cs

WebApiDependencyResolver

using ReactChat.App_Start;
using ReactChat.Controllers;

namespace ReactChat.App_Start
{
    public class WebApiDependencyResolver : IDependencyResolver
    {
        private ChatManager _manager;

        public WebApiDependencyResolver(ChatManager chatManager)
        {
            _manager = chatManager;
        }

        public Object GetService(Type serviceType)
        {
            return serviceType == typeof(ChatController) ? new ChatController(_manager) : null;
        }

        public IEnumerable<Object> GetServices(Type serviceType)
        {
            return new List<Object>();
        }

        public IDependencyScope BeginScope()
        {
            return this;
        }

        public void Dispose()
        {

        }
    }
}

我们需要关注的方法是GetServiceGetService始终会被调用以根据传入请求的类型解析需要调用的服务类型。当请求的服务类型是聊天控制器时,我们将通过将聊天管理器依赖项注入其构造函数来初始化聊天控制器。我们还需要将聊天管理器注入Web API依赖项解析器,以便它可以在依赖项解析器类内部使用。

Global.asax

using System.Web.Http;
using System.Web.Routing;
using ReactChat.Models;
using ReactChat.App_Start;

namespace ReactChat
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            RouteTable.Routes.MapHttpRoute(
                        name: "DefaultApi",
                        routeTemplate: "api/{controller}/{id}",
                        defaults: new { id = System.Web.Http.RouteParameter.Optional }
                        );
        }

        protected void Session_Start(object sender, EventArgs e)
        {
            Session["ChatStore"] = new ChatStore();
            ChatManager chatManager = new ChatManager(Session["ChatStore"] as ChatStore);
            GlobalConfiguration.Configuration.DependencyResolver = 
                                      new WebApiDependencyResolver(chatManager);
        }
    }
}

global.asax文件中,我们需要配置Web API路由,在会话中创建一个新的聊天存储,并设置依赖项解析器配置。

现在是时候移到客户端了。在default.aspx中,首先需要添加所有必需库的引用。在此之后,我们需要一个单独的<div>元素,它将包含整个聊天UI界面。React代码会将整个聊天组件层次结构放在容器div元素内。

最后,我们需要添加已转译组件脚本的引用,该脚本将包含所有转换为ES5格式的React组件。

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="Default.aspx.cs" Inherits="ReactChat.Default" %>

<!DOCTYPE html>
<html>
<head>
    <title>React Chat</title>

    <!-- css -->
    <link rel="stylesheet" href="Style/main.css" />

    <!-- jQuery 2.1.4 -->
    <script src="Scripts/jQuery/jQuery-2.1.4.min.js"></script>

    <!-- signalR -->
    <!--Reference the SignalR library. -->
    <script src="Scripts/jquery.signalR-2.2.0.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="signalr/hubs"></script>

    <!--react-->
    <script src="Scripts/react/react.js"></script>
    <script src="Scripts/react/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.min.js">
    </script>
</head>
<body>
    <div id="container"></div>
    
    <script src="Scripts/React/Components/components.min.js"></script>    
</body>
</html>

React组件

我们将为用户界面的每一个单独元素拥有独立的组件。组件代码将放在它们各自的.jsx文件中。然后,一个gulp任务脚本将使用所有jsx文件中包含的代码来执行各种操作,例如转译、linting、连接、压缩等。

在我开始解释每个组件的作用之前,我认为您应该了解React组件的生命周期。要了解它,您可以参考以下网页,其中解释了如何利用React组件渲染的不同阶段

现在,在我们开始使用它们之前,先创建React组件。

ChatItem.jsx

var ChatItem = React.createClass({    
    render: function () {
        var itemStyle = 'chatItem';
        var userNameStyle = (this.props.source === 'client') ? 
                             'clientUserName' : 'serverUserName';
        var messageStyle = (this.props.source === 'client') ? 
                            'clientMessage' : 'serverMessage';

        return ( <div>
                    <div className={itemStyle}>
                        <div className={userNameStyle}>{this.props.username}</div>
                        <div className={messageStyle}>{this.props.text}</div>
                    </div>
                </div> 
            );
    }
});

这用于显示用户名和聊天消息。用户名和消息文本包含在每个聊天项组件的props集合中。当组件即将渲染时,将读取props中的值以显示给用户。

ChatWindow.jsx

var ChatWindow = React.createClass({    
    sendMessage: function () {
        var $messageInput = $(ReactDOM.findDOMNode(this)).find('input[data-message]');
        var message = $messageInput.val();                
        this.props.sendmessage(message);
        $messageInput.val('');
    },            

    componentDidUpdate: function () {
        var $messageInput = $(ReactDOM.findDOMNode(this)).find('div[data-messages]');
        if($messageInput.length) {            
            $messageInput[0].scrollTop = $messageInput[0].scrollHeight;
        }        
    },

    render: function () {
        var items = [];
        var i=0;
        var userId;
                
        if(this.props.messages.length) {
            for(;i<this.props.messages.length;i++) {
                userId = this.props.messages[i].UserId;
                items.push(<ChatItem 
                                username={this.props.messages[i].UserName}
                                datetime={this.props.messages[i].DateTime} 
                                source={(userId === this.props.userid) ? 'client' : 'server'} 
                                text={this.props.messages[i].Message} key={i} 
                            />);
            }
        }
                
        return ( <div>
                    <div style={{overflow:'hidden'}}> 
                        <div data-messages className={'messagesDiv'}>{items}</div> 
                        <UserList users = {this.props.users}/>
                    </div>
                    <div style={{display:'block',width:'400px'}}>Message: 
                    <input type='text' data-message/> &nbsp; 
                    <a onClick={this.sendMessage} href='#'>Send</a></div>
                </div>
            );
    }
});

聊天窗口组件用于保存所有用户所有聊天消息的集合。聊天窗口组件从其props中读取消息,props将是一个消息对象数组。然后,它在网页上渲染消息项集合。此组件还创建一个活动用户列表,并从props.users集合中读取所有用户名。最底部有一个输入框,供活动用户发送新的聊天消息以发布到全局聊天环境中。

ChatInitialization.jsx

var ChatInitialization = React.createClass({

    initializeUser : function () {
        var $userNameInput = $(ReactDOM.findDOMNode(this)).find('input[data-username]');
        var userName = $userNameInput.val();
        this.props.initialize(userName);
    },
        
    render : function () {
        return ( <div>
                    Enter the user name: 
                    <input type='text' data-username/> &nbsp; 
                    <a onClick={this.initializeUser} href='#'>Start Chatting!</a>
                </div>
            );
    }
});

此组件用于将新用户添加到全局聊天环境中。它所做的就是将新的用户名发送到服务器,然后服务器代码会创建一个具有唯一Id的新用户对象并将其添加到会话中。一个initialize()函数将从其父模块传递到此组件。每当用户输入新名称时,此组件都会调用initialize方法,该方法随后执行位于最顶层组件中的initializeUser()函数中的代码。

UserList.jsx

var UserList = React.createClass({

    render : function () {
        var users = [];
        var i = 0;

        for(;i<this.props.users.length;i++) {
            users.push(<div key={i} className={'userItem'}>{this.props.users[i]}</div>);
        }

        return ( <div style={{overflow:'hidden', display:'block', float:'left', padding:'2px'}}>
                    <h4>Participants</h4>
                    {users}
                 </div> );
    }
});

此组件用于创建活动用户列表。与之前的组件一样,它也从users集合中的props对象获取数据。只有一个render函数,因为它不需要保存任何状态,也不会发生任何用户交互。

MainChat.jsx

var MainChat = React.createClass({
            
    getInitialState : function () {
        return {
            ChatHub: $.connection.MyChatHub,
            Messages: [], 
            UserInitialized: false, 
            UserName:'', 
            UserId:'00000000-0000-0000-0000-000000000000',
            Users: []
        };
    },

    pushNewMessage: function (id, userId, userName, message, dateTime) {
        var msgs = this.state.Messages;
        msgs.push({
            Id: id,
            UserId:userId,
            UserName:userName,
            Message:message,
            DateTime:dateTime
        })
        this.setState({
            Messages: msgs
        });                
    },

    pushUserList: function(userList) {
        this.setState({
            Users: userList
        }); 
    },

    componentWillMount: function () {
        this.state.ChatHub.client.pushNewMessage = this.pushNewMessage;
        this.state.ChatHub.client.pushUserList = this.pushUserList;
        $.connection.hub.start().done(function () { 
            console.log('SignalR Hub Started!');
        });
    },

    initializeUser: function (userName) {   
        var component = this;
        $.getJSON('./api/Chat/?userName=' + userName).then(function (userId) {
            component.setState({
                UserInitialized: true, 
                UserName: userName, 
                UserId: userId            
            });
        });                
    },

    sendMessage: function (message) {
        var messageObj = {
            Id:'00000000-0000-0000-0000-000000000000',
            UserId:this.state.UserId,
            UserName: this.state.UserName, 
            Message: message, 
            DateTime: new Date()
        };
        $.ajax({
            method:'post',
            url: './api/Chat/',
            data: JSON.stringify(messageObj),
            dataType: "json",
            contentType: "application/json; charset=utf-8"
        });
    },

    render: function () {
        if (this.state.UserInitialized) {
            return ( <ChatWindow 
                        messages={this.state.Messages}
                        username={this.state.UserName}
                        userid={this.state.UserId} 
                        sendmessage={this.sendMessage} 
                        users = {this.state.Users} /> 
                    );
        }
        else {
            return ( <ChatInitialization initialize={this.initializeUser}/> );
        }
    }        
});

这是负责渲染每个子组件的最顶层组件。首先,它渲染ChatInitalization组件,并等待新用户在服务器上注册。完成后,它开始显示ChatWindow组件,以便用户可以开始向所有其他连接的用户发送消息。

此组件中有一些函数将被用于与Web API和SignalR模块进行通信。当组件挂载时,将启动SignalR集线器。此组件维护所有消息和当前用户的状态。任何状态更改都将导致整个子层级根据最终的DOM更改重新渲染。

Render.jsx

ReactDOM.render(<MainChat />, document.getElementById('container'));

此文件不包含任何组件,但由于我们必须将chatcomponent渲染到container div中,并且语法是jsx格式,因此代码应放在单独的jsx文件中,以便我们可以使用gulp任务进行转译。

设置和使用Gulp

在我们将jsx语法转换为ES5格式之前,以上所有代码都无法运行。为此,此应用程序使用gulp任务运行器。Gulp将在所有jsx文件上执行许多操作,最后创建一个文件,我们将将其包含在我们的网页中。

在开始编写gulp任务之前,我们需要设置gulp。要设置gulp,我们需要安装Nodejs。由于我是在Windows系统上创建此应用程序的,因此您需要从官方Node网站下载并安装Windows版Node

安装完成后,请按照以下步骤设置gulp以及所需的命令和过滤器

打开命令提示符,并将当前目录更改为您的项目目录。

  • 执行'npm init'来初始化和配置新项目。
  • 执行'npm install gulp --save-dev'来将gulp依赖项安装并保存在您的本地项目目录中。
  • 执行'npm install --save-dev gulp-concat gulp-rename gulp-uglify'来安装gulp插件,用于重命名、连接和压缩JS文件。
  • 执行'npm install --save-dev gulp-babel'来安装gulp babel插件。
  • 执行'npm install --save-dev babel-preset-react'来安装用于React代码的babel预设。
  • 执行'npm install --save-dev babel-preset-es2015'来安装将代码转译为ES5的babel预设。

现在我们需要gulp脚本,其中将包含我们所需的所有任务的代码。为此,向项目添加一个新的JavaScript文件,将其命名为gulpfile.js,并添加以下代码

// Include gulp
var gulp = require('gulp');

// Include Our Plugins
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var babel = require('gulp-babel');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

gulp.task('scripts', function () {
    return gulp.src([
            'Scripts/React/Components/ChatItem.jsx',
            'Scripts/React/Components/ChatInitialization.jsx',
            'Scripts/React/Components/UserList.jsx',
            'Scripts/React/Components/ChatWindow.jsx',
            'Scripts/React/Components/MainChat.jsx',
            'Scripts/React/Components/Render.jsx',
        ])
        .pipe(babel({
            presets: ['react', 'es2015']
        }))
        .pipe(concat('components.js'))
        .pipe(jshint())
        .pipe(rename('components.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest('Scripts/React/Components'));
});

// Watch Files For Changes
gulp.task('watch', function ()
{
    gulp.watch([
            'Scripts/React/Components/ChatItem.jsx',
            'Scripts/React/Components/ChatInitialization.jsx',
            'Scripts/React/Components/UserList.jsx',
            'Scripts/React/Components/ChatWindow.jsx',
            'Scripts/React/Components/MainChat.jsx',
            'Scripts/React/Components/Render.jsx',
        ], ['scripts']);
});

// Default Task
gulp.task('default', ['scripts', 'watch']);

在上面,scripts任务将处理jsx脚本,将它们构建成一个文件。watch任务用于监视脚本文件中的任何新更改,并在发生更改后立即执行另一个构建。

要运行上述脚本,请在项目目录中的命令提示符中执行'gulp'命令。

最后一步是运行应用程序。运行Default.aspx并开始聊天。如果您想对React代码进行任何更改,请确保再次运行gulp任务脚本以构建代码。

随时玩转附带的代码,并根据需要使用它。

关注点

React组件之间可以有多种通信方式。通常,应遵循自顶向下的方法。但如果由于某种原因子组件需要执行父组件中的代码,则可以通过props链传递父方法,或者使用外部全局事件系统注册函数作为事件并在不同区域调用它们。

历史

  • 2016年5月16日:初始版本
© . All rights reserved.