如何用React和Django Channels构建一个聊天应用程序(附代码)

444 阅读7分钟

使用Django来开发HTTP连接和应用程序请求的服务器是很常见的。然而,当开发一个需要连接一直处于开放状态的双向连接的应用程序时,比如会议和聊天程序,使用HTTP连接是低效的。在这种情况下,使用WebSockets是必不可少的。

通过使用WebSockets,所有连接到该开放网络的用户都可以实时接收相关数据,这就提供了一种在客户端和服务器之间建立双向连接的方式。这是一个有状态的协议,意味着在最初的连接认证之后,客户端的凭证被保存下来,在连接被破坏之前不需要进一步认证。

在本教程中,我们将学习如何使用Django和React构建一个聊天应用程序。经过本教程,你应该更熟悉WebSockets在Django和React中的工作方式。

WebSocket的特点

WebSocket是一个双向协议,这意味着数据可以在客户端和服务器之间不间断地即时交换。出于同样的原因,WebSockets也被认为是全双工通信。

WebSockets不需要任何特定的浏览器来工作;所有的浏览器都兼容。WebSocket是一个有状态的协议。由于客户端凭证在主要连接验证后被保存,因此在连接丢失之前不需要再次进行额外的验证。

如何在Django中使用WebSockets

当你想用WebSockets做任何事情时,Django Channels是必不可少的,所以继续用下面的命令安装它:

pip install channels

在这一节中,我们将设置Django以使用WebSockets,并将其与建立一个普通的Django应用程序进行比较。

多亏了Django通道,在Django中使用WebSockets是很简单的。你可以使用Django Channels建立一个ASGI(Asynchronous Server Gateway Interface)服务器,之后你可以建立一个群组,成员之间可以即时发短信。沟通的对象不是某个特定的用户,而是一个组,任何数量的用户都可以加入。

创建一个文件夹,其中将包含你项目的所有代码。在终端上导航到你刚刚创建的文件夹,运行startproject 命令来创建一个新的Django项目:

$ django-admin startproject chat .

现在,运行$ python3 manage.py startapp app ,创建一个新的应用程序。

你需要让你的Django项目意识到一个新的应用程序已经被添加,并且你安装了Channels插件。你可以通过更新chat/settings.py 文件并将'app' 加入到INSTALLED_APPS 列表中来实现。它看起来会像下面的代码:

# project/settings.py
INSTALLED_APPS = [
   ...
   'channels',
   'app',
]

settings.py 文件中,你应该设置配置,以允许 Django 和 Django 频道通过一个消息代理来相互连接。要做到这一点,我们可以利用像Redis这样的工具,但在这个例子中,我们将坚持使用本地后端。在你的settings.py 文件中添加以下一行代码:

ASGI_APPLICATION = "chat.routing.application" #routing.py will handle the ASGI
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': "channels.layers.InMemoryChannelLayer"
        }
    }

在上面的代码中,ASGI_APPLICATION ,需要运行ASGI服务器,并告诉Django在事件发生时该怎么做。我们将把这个配置放在一个名为routing.py 的文件中。路由Django通道与Django URL配置类似;它选择了当WebSocket请求被发送到服务器时要运行的代码。

在创建路由之前,我们首先要开发消费者。在Django通道中,消费者使你能够在你的代码中创建一组函数,每当事件发生时就会被调用。它们类似于Django中的views

要开发消费者,打开app/ 文件夹,创建一个名为consumers.py 的新文件,并粘贴以下代码:

# app/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer

class TextRoomConsumer(WebsocketConsumer):
    def connect(self):

        self.room_name = self.scope\['url_route'\]['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name
        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )
        self.accept()
    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    def receive(self, text_data):
        # Receive message from WebSocket
        text_data_json = json.loads(text_data)
        text = text_data_json['text']
        sender = text_data_json['sender']
        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': text,
                'sender': sender
            }
        )

    def chat_message(self, event):
        # Receive message from room group
        text = event['message']
        sender = event['sender']
        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'text': text,
            'sender': sender
        }))

现在,我们可以创建路由来处理你刚刚创建的消费者。创建一个名为routing.py 的新文件,并粘贴下面的代码,它将协调消费者的工作:

from channels.routing import ProtocolTypeRouter, URLRouter
# import app.routing
from django.urls import re_path
from app.consumers import TextRoomConsumer
websocket_urlpatterns = [
    re_path(r'^ws/(?P<room_name>[^/]+)/$', TextRoomConsumer.as_asgi()),
]
# the websocket will open at 127.0.0.1:8000/ws/<room_name>
application = ProtocolTypeRouter({
    'websocket':
        URLRouter(
            websocket_urlpatterns
        )
    ,
})

构建前台

现在,让我们建立一个聊天应用程序的前端,它使用WebSockets连接到Django的后端。我们将用React来构建这一部分,并且用MUI来添加样式

在你的终端,导航到你的项目根部,运行以下命令以获得React的Create React App模板代码:

npx create-react-app frontend

接下来,cdfrontend/ 目录,并运行以下命令来安装 MUI 和WebSocket依赖项,这使我们能够将 React 应用程序连接到 WebSocket 服务器:

npm install --save --legacy-peer-deps @material-ui/core
npm install websocket

删除frontend/src/App.js 中的所有代码。我们将用本教程其余部分的代码取代它,从初始状态开始:

import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";

class App extends Component {
  state = {
    filledForm: false,
    messages: [],
    value: '',
    name: '',
    room: 'test',
  }
  client = new W3CWebSocket('ws://127.0.0.1:8000/ws/' + this.state.room + '/'); //gets room_name from the state and connects to the backend server 
  render(){
    }

}

现在,我们需要处理组件在浏览器上安装时的情况。我们希望应用程序能够连接到后台服务器,并在组件挂载时获得消息,因此我们将使用componentDidMount() 。你可以通过在render() 函数前粘贴以下代码来实现这一点:

...
componentDidMount() {
    this.client.onopen = () => {
      console.log("WebSocket Client Connected");
    };
    this.client.onmessage = (message) => {
      const dataFromServer = JSON.parse(message.data);
      if (dataFromServer) {
        this.setState((state) => ({
          messages: [
            ...state.messages,
            {
              msg: dataFromServer.text,
              name: dataFromServer.sender,
            },
          ],
        }));
      }
    };
  }
render() {
...

接下来,我们将创建用于更新状态的表单。我们将创建一个表单,用于更新发送者的name 和房间名称。然后,我们将创建另一个表单来处理表单的提交。将下面的代码粘贴在render() 函数中:

render() {
    const { classes } = this.props;
    return (
      <Container component="main" maxWidth="xs">
        {this.state.filledForm ? (
          <div style={{ marginTop: 50 }}>
            Room Name: {this.state.room}
            <Paper
              style={{height: 500, maxHeight: 500, overflow: "auto", boxShadow: "none", }}
            >
              {this.state.messages.map((message) => (
                <>
                  <Card className={classes.root}>
                    <CardHeader title={message.name} subheader={message.msg} />
                  </Card>
                </>
              ))}
            </Paper>
            <form
              className={classes.form}
              noValidate
              onSubmit={this.onButtonClicked}
            >
              <TextField id="outlined-helperText" label="Write text" defaultValue="Default Value"
                variant="outlined"
                value={this.state.value}
                fullWidth
                onChange={(e) => {
                  this.setState({ value: e.target.value });
                  this.value = this.state.value;
                }}
              />
              <Button type="submit" fullWidth variant="contained" color="primary"
                className={classes.submit}
              >
                Send Message
              </Button>
            </form>
          </div>
        ) : (
          <div>
            <CssBaseline />
            <div className={classes.paper}>
              <form
                className={classes.form}
                noValidate
                onSubmit={(value) => this.setState({ filledForm: true })}
              >
                <TextField variant="outlined" margin="normal" required fullWidth label="Room name"
                  name="Room name"
                  autoFocus
                  value={this.state.room}
                  onChange={(e) => {
                    this.setState({ room: e.target.value });
                    this.value = this.state.room;
                  }}
                />
                <TextField variant="outlined" margin="normal" required fullWidth name="sender" label="sender"
                  type="sender"
                  id="sender"
                  value={this.state.name}
                  onChange={(e) => {
                    this.setState({ name: e.target.value });
                    this.value = this.state.name;
                  }}
                />
                <Button type="submit" fullWidth variant="contained" color="primary"
                  className={classes.submit}
                >
                  Submit
                </Button>
              </form>
            </div>
          </div>
        )}
      </Container>
    );
  }

export default withStyles(useStyles)(App);

当你填写房间名称和发件人的名字时,filledForm ,在状态中会被改为true ,然后输入信息的表单会被呈现。在我们的代码中,我们使用了一些MUI类,我们需要导入这些类。你可以通过在你的App.js 文件的顶部粘贴下面的代码来做到这一点:

import Button from "@material-ui/core/Button";
import CssBaseline from "@material-ui/core/CssBaseline";
import TextField from "@material-ui/core/TextField";
import Container from "@material-ui/core/Container";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import Paper from "@material-ui/core/Paper";
import { withStyles } from "@material-ui/core/styles";
const useStyles = (theme) => ({
  submit: {
    margin: theme.spacing(3, 0, 2),
  },
});

一旦消息表单被提交,我们将在点击提交按钮时将文本发送到后端服务器。将下面的代码直接粘贴在componentDidMount() 函数的上方:

  onButtonClicked = (e) => {
    this.client.send(
      JSON.stringify({
        type: "message",
        text: this.state.value,
        sender: this.state.name,
      })
    );
    this.state.value = "";
    e.preventDefault();
  };
  componentDidMount() { 
...

测试应用程序

现在我们已经完成了应用程序的编码,我们可以对它进行测试。首先,通过运行以下命令启动后台服务器。确保你是在manage.py 文件所在的目录中:

python manage.py runserver

在另一个终端窗口,导航到frontend/ 目录,通过运行下面的命令运行前端服务器。React应用程序将自动打开:

npm start

填写一个名字和一个房间名称。然后,在另一个浏览器中以不同的名字但相同的房间名称打开该应用程序。现在,你可以开始和自己聊天了,你会发现信息是实时收到的。

Real Time Chat Application Django Example

结论

在这篇文章中,我们已经了解了WebSockets,它的应用,以及如何通过利用Django通道在Django中使用它。最后,我们介绍了如何使用React建立与Django服务器的WebSocket连接。

尽管我们建立了一个高效的实时聊天应用程序,但仍有一些改进是你可以做到的。例如,为了存储消息,你可以加入一个数据库连接。作为本地后端的替代品,你可以考虑使用Redis作为消息代理。

我希望你喜欢这篇文章,如果你有任何问题,请务必留下评论。编码愉快!