React全家桶+WebSocket實現簡單聊天室

數據通訊的方式有不少種,其中websocket就是一種用於IM的經常使用數據通訊方式,如在線客服、QQ、微信等,或多或少都使用到了這一技術。因此瞭解以及掌握websocket是頗有必要的

廢話很少說,先上圖:css

聊天室

聊天室2

認識WebSocket

WebSocket 是 HTML5 開始提供的一種在單個 TCP 鏈接上進行全雙工通信的協議。html

WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。node

在 WebSocket API 中,瀏覽器和服務器只須要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送。react

在實現過程衝,因爲原生的WebSocket存在兼容問題,因此通常在真正項目開發中,每每會用到一些三方經常使用的庫,如socket.iosocket.io-client等。web

服務器端

簡單地經過nodejs+koa+socket.io去進行模擬實現,其代碼以下:
server.jsjson

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。看下項目總體目錄機構:redux

.
+-- 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:單例實現全應用只有一個其實例對象,代碼以下:bootstrap

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);
相關文章
相關標籤/搜索