一个很棒的实时Demo(Node.js+React.js+Socket.io)

这是一个非常棒的Demo,代码清晰,结构合理,最开始接触ReactJs的时候就发现它了,只是Socket.io还没接触,刚刚了解ReactJs,想学习这个Demo不是一个容易的时请,虽说现在过了一遍Socket.io,读起来还是略有吃力,有些地方需要仔细研究,感觉应该是javascript的知识不牢...想了解Node.js+React.js+Socket.io如何协同工作的小伙伴,这个Demo非常值得学习

程序介绍链接:http://coenraets.org/blog/2015/03/real-time-trader-desktop-with-react-node-js-and-socket-io/ 源码Github下载:https://github.com/ccoenraets/react-trader


推荐阅读:

有了一些基础知识,就可以阅读这个Trader-Demo了

程序Tree如下

运行程序:

npm install 
node server.js

首先看一下server.js文件,也就是程序的入口文件

var express = require('express'),
    app = express(),
    path = require('path'),
    http = require('http').Server(app),
    io = require('socket.io')(http),
    feed = require('./feed');

app.use(express.static(path.join(__dirname, './www')));

io.on('connection', function (socket) {
    console.log('User connected. Socket id %s', socket.id);

    socket.on('join', function (rooms) {
        console.log('Socket %s subscribed to %s', socket.id, rooms);
        if (Array.isArray(rooms)) {
            rooms.forEach(function(room) {
                socket.join(room);
            });
        } else {
            socket.join(rooms);
        }
    });

    socket.on('leave', function (rooms) {
        console.log('Socket %s unsubscribed from %s', socket.id, rooms);
        if (Array.isArray(rooms)) {
            rooms.forEach(function(room) {
                socket.leave(room);
            });
        } else {
            socket.leave(rooms);
        }
    });

    socket.on('disconnect', function () {
        console.log('User disconnected. %s. Socket id %s', socket.id);
    });
});

feed.start(function(room, type, message) {
    io.to(room).emit(type, message);
});

http.listen(3000, function () {
    console.log('listening on: 3000');
});

程序引入了 express框架,通过express框架内置的服务器将./www文件夹变为可访问的,将静态文件放入其中,那么运行程序的时候,通过http模块监听指定端口,在浏览器中输入地址就可以访问www中的文件了

引入socket.io模块,用来监听事件,io.on("connection", function(){})中的connection为socket.IO中的关键字,意思为当有浏览器访问时执行,当一个访客访问时,socket.io为每个访客分配一个唯一的socket对象,用来唯一的标示访客,socket也可以自定义设定监听事件,比如join, leave都是自定义的事件,当浏览器中有相同类型的事件发出,后端就会收到并处理

且先不管这个socket,在没有事件触发时,它是不会执行的,所以继续往下看,有个feed.start()方法,feed是由当前目录下的feed.js文件提供的

var interval,
    onChangeHandler;

var stocks = [
    {symbol: "GM", open: 38.87},
    {symbol: "GE", open: 25.40},
    {symbol: "MCD", open: 97.05},
    {symbol: "UAL", open: 69.45},
    {symbol: "WMT", open: 83.24},
    {symbol: "AAL", open: 55.76},
    {symbol: "LLY", open: 76.12},
    {symbol: "JPM", open: 61.75},
    {symbol: "BAC", open: 15.84},
    {symbol: "BA", open: 154.50}
];

stocks.forEach(function(stock) {
    stock.last = stock.open;
    stock.high = stock.open;
    stock.low = stock.open;
});

function simulateChange() {

    var index = Math.floor(Math.random() * stocks.length),
        stock = stocks[index],

        maxChange = stock.open * 0.005,
        change = maxChange - Math.random() * maxChange * 2,
        last;

    change = Math.round(change * 100) / 100;
    change = change === 0 ? 0.01 : change;

    last = stock.last + change;

    if (last > stock.open * 1.15 || last < stock.open * 0.85)
    {
        change = -change;
        last = stock.last + change;
    }

    stock.change = change;
    stock.last = Math.round(last * 100) / 100;
    if (stock.last > stock.high) {
        stock.high = stock.last;
    }
    if (stock.last < stock.low) {
        stock.low = stock.last;
    }
    onChangeHandler(stock.symbol, 'stock', stock);
}

function start(onChange) {
    onChangeHandler = onChange;
    interval = setInterval(simulateChange, 200);
}

function stop() {
    clearInterval(interval);
}

exports.start = start;
exports.stop = stop;

在feed.js文件中我们看到了start函数,它接受一个函数,并将这个函数绑定到onChangeHandler上,之后设置事件间隔,每200ms执行一次simulateChange()函数

另外,在server.js文件加载feed.js文件的时候就会执行定义stocks数组的操作,数组中初始了一些股票数据

simulateChange()函数中随机的选择一只之前生成的股票,经过一系列的运算,模拟股市股票价格的涨跌,最后调用onChangeHandler(stock.symbol, 'stock', stock),将运算结果传回,也就是传递到了最开始server.js中的feed.start(function(room, type, message) { ... })

最终,由feed.js模拟的数据被送入io.to(room).emit(type, message);

到目前为止,以上介绍的代码功能为每隔200ms,由socket向前端推送最新的股票数据

我们在server.js的start函数中添加console.log(room, type);,重新启动程序,可以看到我们的判断正确,有如下数据持续输出

WMT stock
MCD stock
AAL stock
BAC stock
BA stock
BA stock
JPM stock
LLY stock
BA stock
LLY stock
MCD stock
GE stock
UAL stock
WMT stock
JPM stock
JPM stock

这个type就是我们的设定的一个socket事件,此处值为固定的"stock"

在server.js的最后,监听3000端口,接收来自浏览器的请求

http.listen(3000, function () {
    console.log('listening on: 3000');
});

至此,socket.io一直在尝试向前端发送最新股票信息,http在监听浏览器的访问,程序后端暂时可以放一放,来看一看,当浏览器访问时发生的一些事情及前端的代码实现

先看一下首页——index.html

<!DOCTYPE html>
<html>
<head>
    <title>Trader Desktop</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
    <link rel="stylesheet" href="css/styles.css" type="text/css"/>
    <script src="http://fb.me/react-0.13.1.js"></script>
    <script src="http://fb.me/JSXTransformer-0.13.1.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script src="js/feed-socketio.js"></script>
    <!--<script src="js/feed-mock.js"></script>-->
    <script type="text/jsx" src="js/app.js"></script>
</head>

<body>
<div class="container" id="main"></div>
</body>

</html>

说实话,我第一次看这个首页的时候挺惊讶的,之前虽说学习ReactJs的时候知道很多功能封装到了模块中,但是这么干净清爽的首页却是首次见到...

从首页可以发现,这里只有js/feed-socketio.js与js/app.js两个文件是手动编写的,且app.js的格式为text/jsx,那么它就是用来保存ReactJs代码的,打开查看

var WatchStock = React.createClass({
    getInitialState: function() {
        return {symbol: ""};
    },
    watchStock: function() {
        this.props.watchStockHandler(this.state.symbol);
        this.setState({symbol: ''});
    },
    handleChange: function(event) {
        this.setState({symbol: event.target.value});
    },
    render: function () {
        return (
            <div className="row">
                <p>Available stocks for demo: MCD, BA, BAC, LLY, GM, GE, UAL, WMT, AAL, JPM</p>
                <div className="input-group">
                    <input type="text" className="form-control" placeholder="Comma separated list of stocks to watch..." value={this.state.symbol} onChange={this.handleChange} />
                    <span className="input-group-btn">
                        <button className="btn btn-default" type="button" onClick={this.watchStock}>
                            <span className="glyphicon glyphicon-eye-open" aria-hidden="true"></span> Watch
                        </button>
                    </span>
                </div>
            </div>
        );
    }
});

var StockRow = React.createClass({
    unwatch: function() {
        this.props.unwatchStockHandler(this.props.stock.symbol);
    },
    render: function () {
        var lastClass = '',
            changeClass = 'change-positive',
            iconClass = 'glyphicon glyphicon-triangle-top';
        if (this.props.stock === this.props.last) {
            lastClass = this.props.stock.change < 0 ? 'last-negative' : 'last-positive';
        }
        if (this.props.stock.change < 0) {
            changeClass = 'change-negative';
            iconClass = 'glyphicon glyphicon-triangle-bottom';
        }
        return (
            <tr>
                <td>{this.props.stock.symbol}</td>
                <td>{this.props.stock.open}</td>
                <td className={lastClass}>{this.props.stock.last}</td>
                <td className={changeClass}>{this.props.stock.change} <span className={iconClass} aria-hidden="true"></span></td>
                <td>{this.props.stock.high}</td>
                <td>{this.props.stock.low}</td>
                <td><button type="button" className="btn btn-default btn-sm" onClick={this.unwatch}>
                    <span className="glyphicon glyphicon-eye-close" aria-hidden="true"></span>
                </button></td>
            </tr>
        );
    }
});

var StockTable = React.createClass({
    render: function () {
        var items = [];
        for (var symbol in this.props.stocks) {
            var stock = this.props.stocks[symbol];
            items.push(<StockRow key={stock.symbol} stock={stock} last={this.props.last} unwatchStockHandler={this.props.unwatchStockHandler}/>);
        }
        return (
            <div className="row">
            <table className="table-hover">
                <thead>
                    <tr>
                        <th>Symbol</th>
                        <th>Open</th>
                        <th>Last</th>
                        <th>Change</th>
                        <th>High</th>
                        <th>Low</th>
                        <th>Unwatch</th>
                    </tr>
                </thead>
                <tbody>
                    {items}
                </tbody>
            </table>
            </div>
        );
    }
});

var HomePage = React.createClass({
    getInitialState: function() {
        var stocks = {};
        feed.watch(['MCD', 'BA', 'BAC', 'LLY', 'GM', 'GE', 'UAL', 'WMT', 'AAL', 'JPM']);
        feed.onChange(function(stock) {
            stocks[stock.symbol] = stock;
            this.setState({stocks: stocks, last: stock});
        }.bind(this));
        return {stocks: stocks};
    },
    watchStock: function(symbols) {
        symbols = symbols.replace(/ /g,'');
        var arr = symbols.split(",");
        feed.watch(arr);
    },
    unwatchStock: function(symbol) {
        feed.unwatch(symbol);
        var stocks = this.state.stocks;
        delete stocks[symbol];
        this.setState({stocks: stocks});
    },
    render: function () {
        return (
            <div>
                <WatchStock watchStockHandler={this.watchStock}/>
                <StockTable stocks={this.state.stocks} last={this.state.last} unwatchStockHandler={this.unwatchStock}/>
                <div className="row">
                    <div className="alert alert-warning" role="alert">All stock values are fake and changes are simulated. Do not trade based on the above data.</div>
                </div>
            </div>
        );
    }
});

React.render(<HomePage />, document.getElementById('main'));

最后面,可以看到一个reder,它的功能是将HomePage类填充到id为main的div

往上查看,在其上的React类就是HomePage,类中有一个方法,getInitialState,它的作用为当类被调用的时候进行初始化,只执行一次,我们看看它初始化了什么,先定义了一个空stocks对象,然后出现了feed.watch()函数

feed-socketio.js文件中定义了feed

feed = (function () {

    var socket = io();

    return {
        onChange: function(callback) {
            socket.on('stock', callback);
        },
        watch: function(symbols) {
            socket.emit('join', symbols);
        },
        unwatch: function(symbol) {
            socket.emit('leave', symbol);
        }
    };

}());

这是一个自执行匿名函数,提供了三个方法,分别为feed.onChange(),feed.watch(),feed.unwatch(),并且其内都为socket操作,可见,这个文件提供的功能为socket操作的一个封装

在app.js中传递了一个数组到feed.watch()中,然后socket将数组中的内容推送给了后端,触发join事件,又回到了后端,正是我们之前阅读server.js代码中的如下片段接收数据并处理

    socket.on('join', function (rooms) {
        console.log('Socket %s subscribed to %s', socket.id, rooms);
        if (Array.isArray(rooms)) {
            rooms.forEach(function(room) {
                socket.join(room);
            });
        } else {
            socket.join(rooms);
        }
    });

此处,传递进来的['MCD', 'BA', 'BAC', 'LLY', 'GM', 'GE', 'UAL', 'WMT', 'AAL', 'JPM'],每一个都是上市公司的简称,这一数组被命名为rooms,每个公司的股票就相当于一个room(房间),每个socket代表你的这次访问,那么socket.join(room),就相当于你进入了这个房间,也就是进行“关注”这只股票,同理,leave就相当于取消关注该只股票

我们再回到app.js的HomePage类中继续往下看。

刚才说完feed.watch()设定了我们关注的股票列表,接下来的feed.onChange()函数,传递了一个函数进去,结合feed中的socket.on('stock', callback),可以得知,每当接收到stock事件时就执行callback函数,也就是执行如下两行代码

stocks[stock.symbol] = stock;
this.setState({stocks: stocks, last: stock});

回忆一下,在阅读完后端server.js功能后,我们总结中说的话“socket.io一直在尝试向前端发送最新股票信息” 正是在这里,前端接收到了后端发送过来的数据,通过bind绑定到这个对象,这个feed.onChange()函数,在此处的作用等同于一个socket.on监听

每次将stock数据保存在stocks对象中,并且使用this.setState()方法更新数据,返回最新的stocks对象返回。

在HomePage类中使用render将模板文件插入到index.html中,动态的渲染HTML

在返回的模板中,可以再次调用React类,这样,功能细分,程序就能实现模块化

<div>
    <WatchStock watchStockHandler={this.watchStock}/>
    <StockTable stocks={this.state.stocks} last={this.state.last} unwatchStockHandler={this.unwatchStock}/>
    <div className="row">
        <div className="alert alert-warning" role="alert">All stock values are fake and changes are simulated. Do not trade based on the above data.</div>
    </div>
</div>

  • WatchStock 界面的绿色部分,负责添加关注的股票
  • StockTable 界面的黄色部分,负责显示数据
  • 蓝色部分没有再分配React类控制,直接填充的说明

在调用WatchStock类的时候,后便有一个watchStockHandler,它是HomePage类调用WatchStock类是传递过去的回调函数,绑定在watchStock上,这样做可以将子类中的数据返回。

进入到WatchStock类中,在这个类中,也有个getInitialState用来初始,这里只初始化了一个symbol为空字符串,在WatchStock的render中,又一个input,值的注意的是value与onChange,value的值等于this.state.symbol,onChange是html的内置方法,意思为每当表单中的内容改变时进行响应。它绑定到了this.handleChange,所以当用户输入时,这个handleChange方法就会实时的获得最新的输入,并且通过event.target.value获取到用户的输入,赋值给symbol,因为value的值等于this.state.symbol,这个state的作用就是动态的更新数据,所以用户的输入能及时的反馈在界面上,你可以把value写死一个字符串,再看看输入文字的效果,就能更好的理解此处的state功能了

接下来,值得注意的地方是按钮点击,当输入完成数据,点击的时候,onClick方法会调用this.watchStock,在this.watchStock方法中,通过this.props.watchStockHandler就可以访问我们之前传递到WatchStock的watchStockHandler,将symbol传回父类HomePage中,并设置symbol为空字符串

再回头看HomePage,传递到子类的watchStockHandler即为watchStock,将获取到的symbol简单处理,通过feed.watch进行“关注股票”

接下来的StockRow类与StockTable类同理,理解this.props与this.state的作用就很容易理解React的类数据处理了。


花费了几个小时的整理,算是把代码又过了一遍,代码中很多写法,这在我之前的编程中几乎少有用到,知识可以学习,但是其中的代码组织,清晰的思路,我还学不来,惟有慢慢的积累...

正在摸索Node.js, ReactJs, Socket.IO的朋友,强烈推荐你看看这个Demo,相信一定会有所收获