最近工作比较闲,所以学了点React和Redux相关的东西,然后又做了个简单的在线聊天室练手, 现在就记录下如何用React和Redux来构建一个简单的在线聊天室。
项目在线演示地址:https://murmuring-brook-22426.herokuapp.com/(heroku可以用来免费部署一些自己的小项目玩,还是不错的。 缺点是国内的网访问有点慢。)
源码地址: Github(之后有时间也会陆续加点新功能、完善原有代码)
一、技术栈
1.前端展示层:
React
2.前端数据流管理:
Redux
3.前端样式:
Less
5.前后端通信:
WebSocket开源库socket.io
6.JS语法: ES6, 用
Babel编译
7.包管理:
NPM
8.项目打包:
Webpack
二、项目结构
(因为考虑到后端实现可以很方便切换成别的语言实现,所以并没有把前后端做成一个
同构(isomorphic)应用。)
目录结构:
client目录下分src和build两个文件夹。
- 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.向服务器发送请求
<pre name="code" class="javascript">/**
* server/server.js
*<span style="white-space:pre"> </span> * 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 }); } }); });后台server.js很简单:
利用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.客户端渲染
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,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> ); } }
就这样,一个在线聊天室就完成了。