1. 程式人生 > >用React + Redux + NodeJS 開發一個線上聊天室

用React + Redux + NodeJS 開發一個線上聊天室

最近工作比較閒,所以學了點React和Redux相關的東西,然後又做了個簡單的線上聊天室練手, 現在就記錄下如何用React和Redux來構建一個簡單的線上聊天室。

原始碼地址: Github (之後有時間也會陸續加點新功能、完善原有程式碼)

效果圖:


一、技術棧

1.前端展示層:React 2.前端資料流管理:Redux 3.前端樣式:Less 6.JS語法: ES6, 用Babel編譯 7.包管理: NPM

二、專案結構


(因為考慮到後端實現可以很方便切換成別的語言實現,所以並沒有把前後端做成一個同構(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>
        );
    }
}

就這樣,一個線上聊天室就完成了。