用nodejs和react實現即時通信簡易聊天室功能

命令行建立react 項目

npx create-react-app socketio-demo
複製代碼

進入socketio-demo目錄 運行eject進行拆包,本項目也能夠不拆,這是我的習慣。 注意若是運行eject命令最好在項目初始階段執行,已經開始編寫後不要再使用容易出現bug,新人謹慎使用eject命令css

yarn eject
複製代碼

項目拆包後建立服務器文件夾和文件前端

mkdir server
type null>index.js
複製代碼

建立完成後目錄以下 node

index.png

編寫即時通信(聊天室)後臺

安裝nodejs插件react

npm i express http socket.io nodemon
複製代碼

進入server文件夾下的index.js頁面開始編寫後臺程序ios

const app = require('express')();  
const server = require('http').Server(app);  
const io = require('socket.io')(server);  
//設置端口9093  
server.listen(9093);  

//建立socket.io鏈接  
io.on('connection', function (socket) {  
  //獲取messages事件  
  socket.on('messages', function (data) {  
    //向全部鏈接進行廣播  
  socket.broadcast.emit('messages', data)  
    //對發出者進行廣播,用戶名加上我  
  data.user=data.user+'[我]'  
  socket.emit('messages', data)  
  });  
});
複製代碼

編寫即時通信(聊天室)前臺

後臺編寫完畢,能夠在src目錄中編寫前臺內容 安裝須要用到的react-router和redux依賴git

npm i redux react-redux react-router react-router-dom
複製代碼

在src中建立io文件夾 在io文件夾中建立所須要的文件chrome

cd src
mkdir io
cd io
type null>login.js
type null>socket-demo.js
type null>socket-demo.css
mkdir auth
cd auth
type null>auth.js
複製代碼

建立完成後目錄以下 express

index.png

這裏auth.js文件是用來判斷用戶是否輸入暱稱,如已輸入暱稱能夠進入聊天室,如沒有輸入暱稱則跳回登陸界面要求輸入暱稱npm

本項目當中咱們把暱稱存在redux裏實現登陸界面和聊天室界面的共用,固然現這個項目比較小,若是想用localStorage存在本地也能夠,不過考慮到後期的擴展性以及加深對redux的理解我仍是選擇存在redux當中json

  • src文件夾下建立redux.js文件
  • src文件夾下建立redux文件夾,在redux文件夾下建立user.redux.js文件
cd src
type null>redux.js
mkdir redux
cd redux
type null>user.redux.js
複製代碼

新建目錄以下

index.png
在redux文件夾下的user.redux.js中建立存儲用戶暱稱的reducer

const SET_USERNAME='SET_USERNAME'  
//初始化倉庫  
const initState={user:''}  
//根據動做改變倉庫    
export function User(state = initState, action) {  
  switch (action.type) {  
    case SET_USERNAME:  
      return {...state,user:action.payload}  
    default:  
      return state  
  }  
}  
//寫入暱稱動做  
export function setUserName(user) {  
  return {  
    type:SET_USERNAME,  
  payload:user  
  }  
}
複製代碼

在src/redux.js文件中建立倉庫 combineReducers用於多個reducer的合併,這個項目中也能夠不加,單爲了後期擴展加入使用

import { combineReducers, createStore } from 'redux'  
import {User} from './redux/user.redux'  
//window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 用於chrome redux的擴展項
let reducer = combineReducers({ User })  
let store = createStore(  
  reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())  
  
export default store
複製代碼

這樣就能夠在頁面當中使用redux了

下一步在app.js中引入redux,並把路由搭建起來 在src/app.js中寫入

import React from 'react';
import {HashRouter as Router,Route,Switch} from 'react-router-dom'
import Login from "./io/login";
import SocketDemo from "./io/socket-demo";
import {Provider} from 'react-redux'
import store from './redux'
import Auth from "./io/auth/auth";


function App() {
  return (
    <Provider store={store}>
      <Router>
        <Auth></Auth>
        <Switch>
          <Route exact path='/' component={Login}/>
          <Route exact path='/talk' component={SocketDemo}/>
        </Switch>
      </Router>
    </Provider>
  );
}

export default App;

複製代碼

在寫頁面以前咱們先安裝修飾符插件

npm i babel-plugin-transform-decorators-legacy
複製代碼

Babel >= 7.x 時安裝 @babel/plugin-proposal-decorators

npm i @babel/plugin-proposal-decorators
複製代碼

在package.json中babel項中配置,注意plugins放在presets前不然容易報錯

"babel": {  
  "plugins": [  
    ["@babel/plugin-proposal-decorators", { "legacy": true }]  
  ],  
  "presets": [  
    "react-app"  
  ]  
}
複製代碼

好了這樣就可使用裝飾付了

下面咱們來編寫判斷是否設置用戶名的程序 打開src/io/auth下的auth.js文件

import React from 'react';  
import {connect} from 'react-redux'  
import {withRouter} from 'react-router-dom'  
  
//獲取reducer  
@connect(  
  state=>state,  
  {}  
)  
//獲取router  
@withRouter  
class Auth extends React.Component{  
  componentDidMount() {  
    //若是有用戶名就跳到聊天頁,如沒有則跳到登錄頁。  
  if(this.props.User.user){  
      this.props.history.push('/talk')  
    }else {  
      this.props.history.push('/')  
    }  
  }  
  
  render() {  
    return null  
  }  
}  
  
export default Auth
複製代碼

編寫輸入暱稱並跳轉步驟 打開src/io/login.js文件

import React from 'react';
import './socket-demo.css';
import {connect} from 'react-redux'
import {setUserName} from '../redux/user.redux'

@connect(
  null,
  {setUserName}
)
class Login extends React.Component{
  constructor(props) {
    super(props);
    this.state={
      user:''
    }

    this.login=this.login.bind(this)
    this.onKeyDown=this.onKeyDown.bind(this)
  }

  //鍵盤點擊跳轉  
  onKeyDown(e){
    switch (e.keyCode) {
      case 13:
        this.login();
        return;
      default:
        return;
    }
  }

  //添加鍵盤事件  
  componentDidMount() {
    document.addEventListener("keydown", this.onKeyDown)
  }

  //賦值state  
  handleChange(title,target){
    this.setState({
      [title]:target.target.value
    })
  }

  //賦值並跳轉到聊天室頁面  
  login(){
    let {user}=this.state;
    if(user!==null && user.trim()!==''){
      this.props.setUserName(user);
      this.props.history.push('/talk')
    }
  }

  render() {
    return (
      <div className='loginDiv'>
        <input type='text' placeholder='輸入暱稱' onChange={v=>this.handleChange('user',v)} />
        <button onClick={this.login}>進入聊天室</button>
      </div>  );
  }
}

export default Login
複製代碼

下面是重頭戲,聊天室的前端展現的核心代碼 打開src/iosocket-demo.js文件

import React from 'react'
import io from 'socket.io-client'
import {connect} from 'react-redux'

import './socket-demo.css'

const url='ws://localhost:9093'
const socket = io(url);
@connect(
  state=>state,
  {}
)
class SocketDemo extends React.Component{
  constructor(props) {
    super(props);
    this.state={
      message:'',
      user:this.props.User.user,
      messages:[]
    }
    this.send=this.send.bind(this)
    this.login=this.login.bind(this)
    this.onKeyDown=this.onKeyDown.bind(this)
  }

  componentDidMount() {
    //輸入歡迎信息  
    this.login()
    //增長回車事件  
    document.addEventListener("keydown", this.onKeyDown)
    //socket.io鏈接後臺  
    io(url).on('connect', ()=>{
      console.log('connect');
      socket.on('messages', data => {
        //返回用戶列表  
        this.setState({
          messages:[...this.state.messages,data]
        })
        if(this.refs.showDiv){
          this.refs.showDiv.scrollTop=2000
        }
        
      });
    });
  }
  componentWillUnmount() {
    //斷開socket io鏈接  
    io('ws://localhost:9093').on('disconnect', function(){
      console.log('disconntect');
    });

    document.removeEventListener("keydown", this.onKeyDown)
  }

  //鼠標回車事件  
  onKeyDown(e){
    switch (e.keyCode) {
      case 13:
        this.send();
        return; default:
        return;
    }
  }

  //向後臺發送信息  
  send(){
    let {user,message}=this.state;
    console.log(this.refs.showDiv);

    socket.emit('messages', {user,message});
    this.setState({
      message:''
    })
  }

  login(){
    let user=this.props.User.user;
    const obj={user:'做者',message:`歡迎${user}來到聊天室`}
    if(user.trim()!==''){
      this.setState({
        user:user,
        messages:[obj]
      })
    }
  }

  //賦值state  
  handleChange(title,target){
    this.setState({
      [title]:target.target.value
    })
  }
  render() {
    let cn='showInfo'
    return (
      <div>
        <div className='talkDiv'>
          <div className='operatingDiv'>
            <input  type='text'
                    placeholder='請在此輸入聊天信息'
                    onChange={v=>this.handleChange('message',v)}
                    value={this.state.message}
            />
            <button onClick={this.send}>發送連接</button>
          </div> <div ref='showDiv' className='showDiv'>
          {

            this.state.messages.map((v,index)=>{
              if(index===0){
                cn='titleInfo'
              }else{
                cn='showInfo'
              }
              return (
                <div className={cn} key={index}>
                  <span>{v.user}:</span>
                  <span>{v.message}</span>
                </div>  )
            })
          }
        </div>
        </div>
      </div>  );
  }
}

export default SocketDemo;
複製代碼

最後加上src/iosocket-demo.css

body{  
    background: #008DB7; 
  font-family: 'Microsoft YaHei UI';  
  
}  
.loginDiv{  
    text-align: center;  
  margin: 150px auto 0;  
  width: 250px;  
}  
.loginDiv input[type='text']{  
    display: inline-block;  
  box-sizing: border-box;  
  border-radius: 5px;  
  padding-left: 5px;  
  border: none;  
  width: 250px;  
  height: 35px;  
  line-height: 35px;  
}  
.loginDiv button{  
    display: inline-block;  
  box-sizing: border-box;  
  border-radius: 5px;  
  padding-left: 5px;  
  border: none;  
  width: 250px;  
  height: 35px;  
  line-height: 35px;  
  margin-top: 10px;  
  background: #0067A2; 
  color: #ffffff; 
}  
  
.talkDiv{  
    position: fixed;  
  top: 0;  
  left: 0;  
  right: 0;  
  bottom: 0;  
}  
  
.talkDiv .operatingDiv{  
    position: fixed;  
  bottom: 0;  
  left: 0;  
  right: 0;  
  height: 40px;  
  display: flex;  
}  
  
.talkDiv .operatingDiv input[type='text']{  
    flex: 1;  
  height: 40px;  
  line-height: 40px;  
  box-sizing: border-box;  
  padding-left: 10px;  
}  
.talkDiv .operatingDiv button{  
    display: inline-block;  
  box-sizing: border-box;  
  border-radius: 5px;  
  border: none;  
  width: 250px;  
  height: 40px;  
  line-height: 40px;  
  background: #0067A2; 
  color: #ffffff; 
}  
  
.talkDiv .showDiv{  
    position: fixed;  
  bottom: 40px;  
  left: 0;  
  right: 0;  
  top: 0;  
  font-size: 16px;  
  color: #ffffff; 
  overflow: auto;  
}  
.talkDiv .showDiv .titleInfo{  
    padding: 10px;  
  color: yellow;  
  font-size: 20px;  
}  
.talkDiv .showDiv .showInfo{  
    padding: 10px;  
}
複製代碼

在package.json中加入命令行

"scripts": {  
  "start": "node scripts/start.js",  
  "build": "node scripts/build.js",  
  "server": "nodemon server/index.js"
},
複製代碼
  • 運行後臺 yarn server
  • 運行前臺 yarn start

啓動程序

源碼地址

gitee.com/melissayan/…

預覽地址

http://106.12.186.15:9005

相關文章
相關標籤/搜索