React全家桶+WebSocket实现简单聊天室
数据通信的方式有很多种,其中websocket就是一种用于IM的常用数据通信方式,如在线客服、QQ、微信等,或多或少都使用到了这一技术。所以了解以及掌握websocket是很有必要的
废话不多说,先上图:
认识WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
在实现过程冲,由于原生的WebSocket存在兼容问题,所以一般在真正项目开发中,往往会用到一些三方常用的库,如socket.io
、socket.io-client
等。
服务器端
简单地通过nodejs+koa+socket.io去进行模拟实现,其代码如下:
server.js
const http=require('http'); const Koa=require('koa'); const io=require('socket.io'); const uuid=require('uuid/v4'); // let server=new Koa(); // let httpServer=http.createServer(server.callback()); httpServer.listen(8080); // let wsServer=io.listen(httpServer); wsServer.on('connection', sock=>{ console.log('connected'); const ID=uuid(); sock.emit('ID', ID); sock.on('msg', (user, msg)=>{ wsServer.emit('broadcast', ID, user, msg); }); });
客户端
客户端主要有三部分组成:登录、发送消息、展示消息,其中登录对于的组件是Login.js
,消息相关的组件是Msg.js
。看下项目整体目录机构:
. +-- public +-- node_modules +-- _src | +-- _assets | +-- _components | +-- Login.js | +-- Msg.js | +-- _store | +-- index.js | +-- user.js | +-- msg.js | +-- actions.js | +-- App.js | +-- index.js | +-- socket.js +-- package.json
/src/socket.js:单例实现全应用只有一个其实例对象,代码如下:
import io from 'socket.io-client'; const HOST="ws://localhost:8080/"; export default io(HOST);
src/store/index.js:统一管理不同的reducer,代码如下:
import {createStore,combineReducers} from 'redux' import user from './user' import msg from './msg' export default createStore(combineReducers({ user, msg }))
src/store/user.js:负责管理用户状态(设置用户ID、昵称等)的reducer,代码如下:
import {SET_USER_ID,SET_USER_NAME} from '../actions' export default function(state = {ID:null,name:null},action){ switch(action.type){ case SET_USER_ID: return { ...state, ID:action.value } case SET_USER_NAME: return{ ...state, name:action.value } default: return state } }
src/store/msg.js:负责处理初始化消息、添加消息等状态的reducer,代码如下:
import {ADD_MSG} from '../actions' export default function(state = [],action){ switch(action.type){ case ADD_MSG: return [ ...state, action.msg ] default: return state } } /src/actions.js:统一管理整个应用的所有action,代码如下: //用户相关 export const SET_USER_ID = "set_user_id"; export const SET_USER_NAME = "set_user_name"; //消息相关 export const ADD_MSG = "add_msg"; //------------------------------- export function setUserId(ID){ return{ type:SET_USER_ID, value:ID }; } export function setUserName(name){ return{ type:SET_USER_NAME, value:name }; } export function addMsg(msg){ return{ type:ADD_MSG, msg } }
src/index.js:整个程序的入口,代码如下:
import React from 'react'; import ReactDOM from 'react-dom'; import {BrowserRouter as Router,Route} from 'react-router-dom' import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { async } from 'q'; import {Provider} from 'react-redux' import store from './store' import 'bootstrap/dist/css/bootstrap.css' ReactDOM.render( ( <Provider store={store}> <Router> <Route component={App}/> </Router> </Provider> ), document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
src/App.js:应用的主组件,负责路由的配置,数据的初始化,代码如下:
import React, { Component } from 'react'; import {connect} from 'react-redux' import {Route,Redirect} from 'react-router-dom' import Login from './components/Login' import Msg from './components/Msg' import {setUserId,addMsg} from './actions' import socket from './socket' class App extends Component { constructor(){ super() } componentDidMount(){ //得到初始化用户ID socket.on('ID',(ID) => { this.props.setUserId(ID); }) //接收消息 socket.on('broadcast',(from, fromUser, msg) => { this.props.addMsg({from, fromUser, msg}) }) } render() { return ( <div> { this.props.user.name ? '':( <Redirect to="/"/> ) } <Route path="/" exact component={Login}/> <Route path="/msg" component={Msg}/> </div> ); } } export default connect((state,props) => Object.assign({},props,state),{ setUserId, addMsg })(App);
src/components/Login.js:登录组件,代码如下:
import React, { Component } from 'react'; import {connect} from 'react-redux' import {setUserName} from '../actions' class Login extends Component { constructor(){ super(); this.rnd = Math.floor(Math.random()*1000000); } login = () => { let username = this.refs.username.value; this.props.setUserName(username); this.refs.username.value = ""; this.props.history.push('/msg'); } render() { return ( <div className="panel panel-primary"> <div className="panel-heading"> <h2 className="panel-title"> 登录 </h2> </div> <div className="panel-body"> <div className="from-group"> <label htmlFor={'username'+this.rnd}></label> <input className="from-control" type="text" id={'username'+this.rnd} ref="username" placeholder="请输入用户名"/> </div> <div className="from-group" style={{'marginTop':'10px'}}> <button type="button" className="btn btn-default" onClick={this.login}>登录</button> </div> </div> </div> ); } } export default connect((state,props) => Object.assign({},props,state),{ setUserName })(Login);
src/components/Msg.js:发送消息、展示消息的组件,代码如下:
import React, { Component } from 'react'; import {connect} from 'react-redux' import msg from '../store/msg'; import socket from '../socket' class Msg extends Component { send = () => { console.log('this.props.user.username:',this.props.user.name) socket.emit('msg',this.props.user.name,this.refs.msg.value); this.refs.msg.value = "" } render() { return ( <div> <div> <div className="from-group"> <textarea className="from-control" ref="msg"></textarea> </div> <div className="from-group"> <button className="btn btn-default" type="button" onClick={this.send}>发送</button> </div> </div> <ul> { this.props.msg.map(({from,fromUser,msg},index) => ( <li key={index}> <h3 className="list-group-item-heading" style={{color:from==this.props.user.ID ? 'red':''}}>{fromUser}</h3> <p className="list-group-item-text">{msg}</p> </li> )) } </ul> </div> ); } } export default connect((state,props) => Object.assign({},props,state),{ })(Msg);