牛骨文教育服务平台(让学习变的简单)
博文笔记

用React + Redux + NodeJS 开发一个在线聊天室

创建时间:2016-09-29 投稿人: 浏览次数:7431

最近工作比较闲,所以学了点React和Redux相关的东西,然后又做了个简单的在线聊天室练手, 现在就记录下如何用React和Redux来构建一个简单的在线聊天室。

项目在线演示地址:https://murmuring-brook-22426.herokuapp.com/  (heroku可以用来免费部署一些自己的小项目玩,还是不错的。 缺点是国内的网访问有点慢。)

源码地址: Github (之后有时间也会陆续加点新功能、完善原有代码)

效果图:





1.前端展示层:React 2.前端数据流管理:Redux 3.前端样式:Less 4.后端: Node.js Express框架 5.前后端通信: WebSocket开源库socket.io 6.JS语法: ES6, 用Babel编译 7.包管理: NPM 8.项目打包: Webpack





(因为考虑到后端实现可以很方便切换成别的语言实现,所以并没有把前后端做成一个同构(isomorphic)应用。)

目录结构:  


client目录下分src和build两个文件夹。


build文件夹是项目打包后生成的。
src下面分html、js、less三个文件夹, html存放页面最原始的html,less存放样式文件,js目录下则是主要的前端逻辑。
  • actions目录下存放Redux框架中action的部分
  • components目录下存放用React写的各个组件
  • constants目录下存放项目里的一些常量
  • containers目录下存放对React组件的封装,这种封装是React和Redux链接的桥梁
  • pages目录下存放webpack打包的entries
  • reducers目录下存放Redux框架中reducer的部分
  • routes目录下存放React框架中路由管理的部分

server目录下放的是后台逻辑

特别简单,就一个server.js,用来接收用户请求,并返回响应。

package.json和webpack配置文件是NPM+Webpack组合的配置文件,负责包管理、打包。 (使用NPM + Webpack进行前端开发的示例)


1.向服务器发送请求

后台server.js很简单:

/**
 * Created by hshen on 9/23/16.
 */

// Import modules
var express = require("express");
var path = require("path");
var ejs = require("ejs");

// Create server
var app = express()
  , server = require("http").createServer(app)
  , io = require("socket.io").listen(server);
var port = process.env.PORT || 3000;
server.listen(port);

// Return index.html for "/"
app.get("/", function (req, res) {
    res.render("index");
});

// Set path for views and static resources
app.set("views", "./client/src/html");
app.set("view engine", "html");
app.engine("html", ejs.renderFile);
app.use("/static", express.static("./client/build"));

var userNumber = 0;

io.sockets.on("connection", function (socket) {
    var signedIn = false;

    socket.on("newMessage", function (text) {
        io.sockets.emit("newMessage",{
            userName: socket.userName,
            text: text
        })
    });

    socket.on("signIn", function (userName) {
        if (signedIn) return;

        // we store the username in the socket session for this client
        socket.userName = userName;
        ++userNumber;
        signedIn = true;

        io.sockets.emit("userJoined", {
            userName: userName,
            userNumber: userNumber
        });
    });

    socket.on("disconnect", function () {
        if (signedIn) {
            --userNumber;

            io.sockets.emit("userLeft", {
                userName: socket.userName,
                userNumber: userNumber
            });
        }
    });

});
对相应的请求路径返回相应的内容:  对"/"默认路径返回index.html, 同时定义static路径(static路径在html中用到)对应源代码中的哪个文件夹。 利用socket.io库,监听websocket连接,对连接发出的事件进行响应,广播给别的连接知晓。

<!--client/src/html/index.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chatting Room</title>
</head>
<body>

<div class="main"></div>

<script type="text/javascript" src="static/js/common.js"></script>
<script type="text/javascript" src="static/js/index.js"></script>

</body>
</html>
用户在访问‘/’路径后,拿到的是如上的html,这里将会再去服务器请求打包后的index.js和common.js。
index.js和common.js是webpack根据配置打包一系列js而成的,这时候浏览器会再发请求去获取。(这样的确影响前端性能,因此我觉得更好的做法是服务器端渲染第一步,之后都在客户端进行路由跳转、渲染。)


2.客户端渲染

这部分内容也就是主要的React+Redux这部分了。

1)利用Provider来使React连接Redux的store

// js/pages/index.js , 项目入口文件

/**
 * Created by hshen on 9/20/16.
 */

import $ from "jquery"
import React from "react"
import { render } from "react-dom"
import { createStore } from "redux"
import { Provider } from "react-redux"
import reducer from "js/reducers"
import Routes from "js/routes"

let store = createStore(reducer)

import "css/main.less"

render(
    <Provider store={store}>
        {Routes}
    </Provider>,
    $(".main")[0]
);

2)用react-router来做路由

/**
 * Created by hshen on 9/24/16.
 */

import React from "react"
import ChatContainer from "js/containers/ChatContainer";
import SignInContainer from "js/containers/SignInContainer";

import { Router, Route, IndexRoute, browserHistory } from "react-router";

const Routes = (
    <Router history={browserHistory}>
        <Route path="/" component={SignInContainer} />
        <Route path="/chat" component={ChatContainer}></Route>
    </Router>
);

export default Routes;

3)在Container中利用connect方法获取Redux的state和actions

/**
 * Created by hshen on 9/24/16.
 */

import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import {bindActionCreators} from "redux";

import Chat from "js/components/chat/Chat";
import * as actions from "js/actions/actions";

class ChatContainer extends Component {
    render() {
        return (
            <Chat {...this.props} />
        );
    }
}

const mapStateToProps = (state, ownProps) => {
    return {
        messages: state.messages,
    }
}

const mapDispatchToProps = (dispatch, ownProps) => {
    return bindActionCreators({
        receiveMessage: actions.receiveMessage,
        sendMessage: actions.sendMessage,
        userJoined: actions.userJoined,
        userLeft: actions.userLeft
    },dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(ChatContainer)

4)在Component中对数据进行渲染

/**
 * Created by hshen on 9/24/16.
 */

import React, { Component, PropTypes } from "react";
import MessageInput from "js/components/chat/MessageInput";
import MessageItem from "js/components/chat/MessageItem";
import Singleton from "js/socket"

import "css/chat.less"

export default class Chat extends Component {

    constructor(props, context) {
        super(props, context);
        this.socket = Singleton.getInstance();
    }

    componentWillMount() {
        const { receiveMessage, userJoined, userLeft } = this.props;
        this.socket.on("newMessage",function (msg) {
            receiveMessage(msg);
        });
        this.socket.on("userJoined",function (data) {
            console.log(data);
            userJoined(data);
        });
        this.socket.on("userLeft",function (data) {
            console.log(data);
            userLeft(data);
        });
    }

    sendMessage(newMessage) {
        const { sendMessage, userName } = this.props;
        if (newMessage.length !== 0) {
            sendMessage(newMessage);
            this.socket.emit("newMessage", newMessage);
        }
    }

    render() {
        const { messages} = this.props;
        return (
            <div className="chat">
                <div className="chat-area">
                    <ul className="messages">
                        {messages.map( (message, index) =>
                            <MessageItem message={message} key={index}/>
                        )}
                    </ul>
                </div>
                <MessageInput sendMessage={this.sendMessage.bind(this)} />
            </div>
        );
    }
}

就这样,一个在线聊天室就完成了。
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。