React入门实战:利用React和Socket.io构建实时协作应用

1.背景介绍

在现代互联网中,每天都产生海量的数据,这些数据对于人类的很多活动都是至关重要的。而协同工作也成为实现组织成功的一项关键环节。然而在实际工作当中,我们发现人们对协同工作的需求并不强烈,很少有人真正愿意花时间去做这种重复性劳动。因此,如何帮助人们更有效地、更高效地完成协同工作,是企业必不可少的技术需求之一。 为了解决这一问题,最近很热的一个领域就是Web开发技术的更新换代。基于Javascript的React框架,以及其生态圈,正在成为许多企业不可或缺的选择。由于其轻量级、组件化、声明式编程特性,以及丰富的插件支持库,React被认为是非常适合于搭建企业级应用的最佳方案。另外,由于WebSocket协议的快速发展和广泛应用,也可以说React+WebSocket可以实现高度实时的应用场景。因此,本文将详细介绍如何利用React和Socket.io来实现一个简单的实时协作应用。

2.核心概念与联系

Socket.io

首先需要了解一下Socket.io,它是一个基于Websocket协议的实时通信库。相比于传统的HTTP轮询方式,通过Socket.io可以将服务端推送给客户端的数据进行即时传输。同时,Socket.io还提供了多种实用工具及API,如Rooms、Namespaces等,可以让复杂的应用场景变得更加灵活。如下图所示,Socket.io可简化Web应用程序的创建流程,并提供对实时数据的处理能力。

Firebase

另外,本文还会涉及到Firebase作为后端云服务提供商。Firebase是一款专注于移动应用、Web前端、后台开发的平台。它提供像身份验证、数据库、推送通知、文件存储、函数计算、静态托管等一系列功能,能够帮助用户实现前后端集成、安全数据存储、消息推送、应用性能监控等诸多功能。借助Firebase,可以快速建立实时协作应用。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

首先,需要注册一个Firebase账号。然后创建一个新的项目,添加一个Web应用即可。接着配置好 Firebase Cloud Messaging (FCM) ,并启用 Firebase 应用。最后,安装 Socket.io 的 npm 模块并进行相应的设置。

下一步,我们需要搭建 React + Socket.io 框架,并实现前端与后端之间的实时通信。首先,在 React 中引入 socket.io-client 模块。然后,初始化 Socket 对象,指定服务器地址以及端口号。定义事件监听器,用于接收来自服务器的数据。

import io from'socket.io-client';

const socket = io('http://localhost:3000'); // 设置服务器地址

// 定义事件监听器
socket.on('message', function(msg){
  console.log(`收到来自服务器的消息:${msg}`);
});

此外,还需要定义一个回调函数,用来向服务器发送数据。可以通过调用 emit() 方法来实现,并传入事件名称和数据。例如:

function sendMsg(){
  const msg = document.getElementById("msg").value;
  socket.emit('message', msg);
}

最后,还需要编写后端的代码,监听来自前端的连接请求,并返回欢迎信息。代码如下:

const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server);

// 在线用户列表
let onlineUsers = [];

// 绑定连接事件
io.on('connection', function(socket){

  let username = "";

  // 用户登录
  socket.on('login', function(_username){
    if(!_username || typeof _username!== "string") {
      return;
    }

    username = _username;

    if (!onlineUsers.includes(username)) {
      onlineUsers.push(username);
      updateOnlineList();
    }

    // 向当前用户发送欢迎信息
    io.to(socket.id).emit('welcome', `欢迎 ${username} 来到聊天室!`);

    // 向其他在线用户发送新用户上线消息
    broadcastNewUserMessage({type: "newUser", user: username });
  });

  // 用户退出
  socket.on('logout', function(){
    if(onlineUsers.includes(username)){
      onlineUsers.splice(onlineUsers.indexOf(username), 1);
      updateOnlineList();
    }

    broadcastOfflineUserMessage({ type: "offlineUser", user: username });
  });

  // 用户发送消息
  socket.on('send message', function(data){
    data.user = username;
    broadcastChatMessage(data);
  })

  // 更新在线用户列表
  function updateOnlineList() {
    io.sockets.emit('update online users', onlineUsers);
  }

  // 向所有在线用户发送消息
  function broadcastChatMessage(data) {
    io.sockets.emit('receive message', data);
  }

  // 向所有在线用户发送新用户上线消息
  function broadcastNewUserMessage(data) {
    io.sockets.emit('new user', data);
  }

  // 向所有在线用户发送用户下线消息
  function broadcastOfflineUserMessage(data) {
    io.sockets.emit('offline user', data);
  }

});

server.listen(process.env.PORT || 3000, () => {
  console.log('Server listening on port 3000');
});

4.具体代码实例和详细解释说明

HTML 文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>实时协作应用</title>
</head>
<body>
  <div id="chatroom">
    <h2>实时协作应用</h2>
    <input type="text" placeholder="输入用户名" id="username">
    <textarea rows="10" cols="50" placeholder="输入消息..." id="msg"></textarea>
    <button οnclick="sendMsg()">发送</button>
    <ul id="onlineUserList"></ul>
  </div>

  <!-- React -->
  <script src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
  <!-- Socket.io -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.slim.js"></script>
  <!-- index.js -->
  <script src="./index.js"></script>
</body>
</html>

CSS 文件

* {
  box-sizing: border-box;
}

#chatroom {
  margin: auto;
  width: 50%;
  padding: 20px;
  text-align: center;
}

h2 {
  font-size: 24px;
  color: #333;
  margin-bottom: 20px;
}

#username, #msg {
  display: block;
  width: 100%;
  height: 32px;
  margin-bottom: 10px;
  padding: 6px 12px;
  font-size: 14px;
  line-height: 1.42857143;
  color: #555;
  background-color: #fff;
  background-image: none;
  border: 1px solid #ccc;
  border-radius: 4px;
  -webkit-transition: border-color ease-in-out.15s,-webkit-box-shadow ease-in-out.15s;
       -o-transition: border-color ease-in-out.15s,box-shadow ease-in-out.15s;
          transition: border-color ease-in-out.15s,box-shadow ease-in-out.15s;
}

#msg {
  resize: vertical;
}

button {
  display: inline-block;
  margin-bottom: 0;
  font-weight: normal;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  background-image: none;
  border: 1px solid transparent;
  white-space: nowrap;
  padding: 6px 12px;
  font-size: 14px;
  line-height: 1.42857143;
  border-radius: 4px;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  color: #333;
  background-color: #fff;
  border-color: #ccc;
}

button:hover {
  background-color: #eee;
  border-color: #aaa;
}

ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

li {
  float: left;
  margin-right: 10px;
}

li img {
  max-width: 50px;
  border-radius: 50%;
}

span {
  position: relative;
  top: 3px;
  font-size: 12px;
}

JavaScript 文件(index.js)

class App extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      messages: [], // 消息列表
      users: []    // 在线用户列表
    };

    // 初始化 Socket 对象
    this.socket = io('http://localhost:3000');

    // 绑定事件监听器
    this.socket.on('connect', () => {
      console.log('Connected to the server!');

      // 获取历史消息记录
      this.getHistoryMessages();
    });

    this.socket.on('disconnect', () => {
      console.log('Disconnected from the server...');
    });

    this.socket.on('receive message', (data) => {
      this.addMessage(data);
    });

    this.socket.on('new user', (data) => {
      this.addUser(data);
    });

    this.socket.on('offline user', (data) => {
      this.removeUser(data);
    });

    this.socket.on('update online users', (users) => {
      this.setState({
        users: users
      });
    });
  }

  getHistoryMessages = () => {
    fetch('/api/messages')
   .then((response) => response.json())
   .then((data) => {
      this.setState({
        messages: data
      });
    })
   .catch((error) => {
      console.error(error);
    });
  }

  addMessage = (data) => {
    const messages = [...this.state.messages];
    messages.push(data);
    this.setState({
      messages: messages
    });

    setTimeout(() => {
      this.scrollToBottom();
    }, 100);
  }

  addUser = (data) => {
    const users = [...this.state.users];
    if (!users.includes(data.user)) {
      users.push(data.user);
    }
    this.setState({
      users: users
    });
  }

  removeUser = (data) => {
    const users = [...this.state.users];
    const index = users.findIndex((user) => user === data.user);
    if (index > -1) {
      users.splice(index, 1);
    }
    this.setState({
      users: users
    });
  }

  scrollToBottom = () => {
    const node = ReactDOM.findDOMNode(this.refs.scrollableDiv);
    node.scrollTop = node.scrollHeight;
  }

  handleSubmit = (event) => {
    event.preventDefault();
    const inputText = this.refs.textInput.value.trim();

    if (inputText!== '') {
      const newMessage = {
        text: inputText,
        createdAt: Date.now(),
        user: ''
      };

      this.refs.textInput.value = '';

      this.socket.emit('send message', newMessage);
    }
  }

  render() {
    return (
      <div className="App" style={{display: 'flex'}}>
        <div className="left-panel" style={{flexGrow: '1', overflowY: 'auto'}} ref='scrollableDiv'>
          {
            this.state.messages.map((message, index) => {
              return (
                <p key={index}>
                  <strong>{message.user}</strong>: {message.text} <small>({moment(message.createdAt).format('HH:mm:ss A DD/MM/YYYY')})</small>
                </p>
              );
            })
          }
        </div>

        <div className="right-panel" style={{flexGrow: '0', maxWidth: '300px', paddingRight: '20px'}}>
          <h2>在线用户</h2>
          {
            this.state.users.length > 0? 
              this.state.users.map((user, index) => {
                return (
                )
              }) : 
              (<p>没有在线用户</p>)
          }

          {/* 搜索框 */}
          <form onSubmit={(event) => this.handleSearch(event)}>
            <label htmlFor="searchInput"><i class="fa fa-search"></i></label>
            <input type="text" name="searchInput" id="searchInput" placeholder="搜索用户"/>
          </form>
        </div>

        {/* 提交消息 */}
        <div className="center-panel">
          <h2>发表留言</h2>
          <form onSubmit={(event) => this.handleSubmit(event)}>
            <input type="text" ref="textInput" placeholder="输入消息..."/>
            <button type="submit">发送</button>
          </form>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

API 文件(api.js)

module.exports = (app) => {
  app.get('/api/messages', async (req, res) => {
    try {
      const queryResult = await MessageModel.find().sort('-createdAt');
      res.status(200).json(queryResult);
    } catch (err) {
      console.error(err);
      res.status(500).json(err);
    }
  });

  // 添加消息路由
  app.post('/api/messages', async (req, res) => {
    try {
      const messageData = req.body;
      const message = new MessageModel(messageData);
      await message.save();
      res.status(200).json(message);
    } catch (err) {
      console.error(err);
      res.status(500).json(err);
    }
  });
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI架构设计之禅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值