React全家桶WebSocket实现简单聊天室

39次阅读

共计 5536 个字符,预计需要花费 14 分钟才能阅读完成。

数据通信的方式有很多种,其中 websocket 就是一种用于 IM 的常用数据通信方式,如在线客服、QQ、微信等,或多或少都使用到了这一技术。所以了解以及掌握 websocket 是很有必要的

废话不多说,先上图:

认识 WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

在实现过程冲,由于原生的 WebSocket 存在兼容问题,所以一般在真正项目开发中,往往会用到一些三方常用的库,如 socket.iosocket.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);

正文完
 0