react+react-router+redux+Node.js+socket.io寫一個聊天webapp

大翰仔仔發表於2019-05-02

一、專案預覽

之前看一個寫聊天器的教程,自己也跟著教程做了一遍,由於懶得去找圖片和一些圖示我就用教程中的素材來做,主要是用了react+react-router+redux+Node.js+socket.io的技術棧,接下來就是專案的預覽

1.首先在/login下能看到有登入和註冊按鈕

登入註冊頁

2.點選註冊按鈕,路由跳到/register,註冊一個賬號,使用者和密碼都為LHH,選擇“牛人”,點選註冊,之後路由會跳到/geniusinfo,即牛人完善資訊頁,選擇一個頭像並完善資訊後點選儲存按鈕

註冊頁
完善資訊頁

3.可以看到已經進入有三個tab選項的內容頁面了,點選“我”,路由跳轉到/me即可看到個人中心內容,但此時boss和訊息的tab頁仍沒有內容,可以按照之前步驟註冊一個Boss賬號,只需在註冊的時候選擇Boss選項

個人中心

4.現在在LHH和LCE賬號分別能看到的列表

列表

5.點選進入聊天室,輸入內容

聊天室

二、接下來對專案的主要內容進行解釋

1.專案的除掉node_modules後的目錄
├─build
│  └─static
│      ├─css
│      └─js
├─config
│  └─jest
├─public
├─scripts
├─server
└─src
    ├─component
    │  ├─authroute
    │  ├─avatar-selector
    │  ├─boss
    │  ├─chat
    │  ├─dashboard
    │  ├─genius
    │  ├─img
    │  ├─logo
    │  ├─msg
    │  ├─navlink
    │  │  └─img
    │  ├─user
    │  └─usercard
    ├─container
    │  ├─bossinfo
    │  ├─geniusinfo
    │  ├─login
    │  └─register
    └─redux
複製程式碼

其中build資料夾的內容為npm run build打包後的內容,在專案中如果啟用後端介面也可訪問

2.入口頁面
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
// eslint-disable-next-line
import { BrowserRouter } from 'react-router-dom';
import App from './app'

import reducers from './reducer'
import './config'
import './index.css'

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
    window.devToolsExtension?window.devToolsExtension():f=>f
))

// boss genius me msg 4個頁面
ReactDOM.render(
    (<Provider store={store}>
        <BrowserRouter>
            <App></App>
        </BrowserRouter>
    </Provider> ),
    document.getElementById('root')
 )
複製程式碼

使用react-redux的Provider,可實現全域性的狀態儲存,子元件可通過props獲得儲存在全域性的狀態

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
    window.devToolsExtension?window.devToolsExtension():f=>f
))
複製程式碼

上面程式碼的主要作用是關於配置瀏覽器的redux外掛的,可以通過這個外掛在控制檯中檢視state中的資料。
來看下app.js中的程式碼

import React from 'react'
import Login from './container/login/login.js';
import Register from './container/register/register.js';
import AuthRoute from './component/authroute/authroute.js';
import BossInfo from './container/bossinfo/bossinfo.js';
import Geniusinfo from './container/geniusinfo/geniusinfo';
import Dashboard from './component/dashboard/dashboard';
import Chat from './component/chat/chat'
import {  Route,  Switch } from 'react-router-dom';

class App extends React.Component{
    render() {
        return (
            <div>
                <AuthRoute></AuthRoute>
                <Switch>
                    <Route path='/bossinfo' component={BossInfo}></Route>
                    <Route path='/geniusinfo' component={Geniusinfo}></Route>
                    <Route path='/login' component={Login}></Route>
                    <Route path='/register' component={Register}></Route>
                    <Route path='/chat/:user' component={Chat}></Route>
                    <Route component={Dashboard}></Route>
                </Switch>
                
            </div>
        )
    }
}
export default App
複製程式碼

這裡主要是講主頁面中的程式碼分割出來。
authroute.js中是路由跳轉的邏輯判斷
頁面中的UI元件也用到了antd-mobile外掛
客戶端接收和傳送資料得引入socket.io-client,程式碼在chat.redux.js中。
聊天器中需要儲存在資料庫的內容主要為from(傳送端)、to(接收端)、read(是否已讀)、content(聊天內容)、create_time(聊天時間)而且還需要一個唯一的chatid來代表這個聊天室的唯一性,可以用fromto拼接,拼接函式寫在util.js中。

3.Server

後端介面用到了node.jsexpress框架,資料庫用到了mongodb,在server資料夾中存放連線資料庫的檔案,model.js在直接與mongodb資料庫連線,

const mongoose = require('mongoose');
// 連線mongo,並且使用my_app這個集合
const DB_URL = "mongodb://localhost:27017/chat_app";
mongoose.connect(DB_URL);

const models = {
    user: {
        'user': { 'type': String, 'require': true },
        'pwd': { 'type': String, 'require': true },
        'type': { 'type': String, 'require': true },
        // 頭像
        'avatar': { 'type': String },
        // 個人簡介或者職位簡介
        'desc': { 'type': String },
        // 職位名
        'title': { 'type': String },
        // 如果是boss,還有兩個欄位
        'company': { 'type': String },
        'money': { 'type': String }
    },
    chat: {
        'chatid': { 'type': String, 'require': true },
        'from': { 'type': String, 'rewuire': true },
        'to': { 'type': String, 'require': true },
        'read': { 'type': String, 'require': true },
        'content': { 'type': String, 'require': true, 'default': '' },
        'create_time': { 'type': Number, 'default': new Date().getTime() }
    }
}
for (let m in models) {
    mongoose.model(m, new mongoose.Schema(models[m]))
}
module.exports = {
    getModel: function(name) {
        return mongoose.model(name)
    }
}
複製程式碼

連線的資料庫埠號為27017,這個視自己電腦的資料庫埠號而定。 在server.js中引入了http、express、socket.io外掛,服務端用的是9093埠,

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const model = require('./model')
    // const User = model.getModel('user');
const Chat = model.getModel('chat');
const path = require('path')
const app = express();
//work with express
const server = require('http').Server(app);
const io = require('socket.io')(server);
io.on('connection', function(socket) {
    // console.log('user login')
    socket.on('sendmsg', function(data) {
        const { from, to, msg } = data;
        const chatid = [from, to].sort().join('_');
        Chat.create({ chatid, from, to, content: msg }, function(err, doc) {
                // console.log(doc._doc)
                io.emit('recvmsg', Object.assign({}, doc._doc))
            })
            // console.log(data);
            // io.emit('recvmsg', data)
    })
})
const userRouter = require('./user');
app.use(cookieParser());
app.use(bodyParser.json())
app.use('/user', userRouter);
app.use(function(req, res, next) {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
server.listen(9093, function() {
    console.log('Node app start at port 9093')
});
複製程式碼

客戶端用到的介面寫在user.js

const express = require('express')
const Router = express.Router();
const model = require('./model')
const User = model.getModel('user');
const Chat = model.getModel('chat');
const _filter = { 'pwd': 0, '__v': 0 };

// 刪除所有聊天記錄
// Chat.remove({}, function(e, d) {})
// 加密
const utils = require('utility');
Router.get('/list', function(req, res) {
    const { type } = req.query
    // 刪除所有使用者
    // User.remove({}, function(e, d) {})
    User.find({ type }, _filter, function(err, doc) {
        return res.json({ code: 0, data: doc })
    })
});
Router.get('/getmsglist', function(req, res) {
    const user = req.cookies.userid;
    User.find({}, function(err, userdoc) {
        let users = {};
        userdoc.forEach(v => {
            users[v._id] = { name: v.user, avatar: v.avatar }
        })
        Chat.find({ '$or': [{ from: user }, { to: user }] }, function(err, doc) {
            // console.log(doc)
            if (!err) {
                return res.json({ code: 0, msgs: doc, users: users })
            }
        })
    })
})
Router.post('/readmsg', function(req, res) {
    const userid = req.cookies.userid;
    const { from } = req.body;
    // console.log(userid, from)
    Chat.update({ from, to: userid }, { '$set': { read: true } }, { 'multi': true },

        function(err, doc) {
            if (!err) {
                return res.json({ code: 0, num: doc.nModified })
            }
            return res.json({ code: 1, msg: '修改失敗' })
        })
})
Router.post('/update', function(req, res) {
    const userid = req.cookies.userid;
    if (!userid) {
        return json.dumps({ code: 1 });
    }
    const body = req.body;
    User.findByIdAndUpdate(userid, body, function(err, doc) {
        const data = Object.assign({}, {
            user: doc.user,
            type: doc.type
        }, body)
        return res.json({ code: 0, data })
    })
});
Router.post('/login', function(req, res) {
    const { user, pwd } = req.body;
    User.findOne({ user, pwd: md5Pwd(pwd) }, _filter, function(err, doc) {
        if (!doc) {
            return res.json({ code: 1, msg: '使用者名稱或者密碼錯誤' });
        }
        res.cookie('userid', doc._id)
        return res.json({ code: 0, data: doc })
    })
});
Router.post('/register', function(req, res) {
    console.log(req.body);
    const { user, pwd, type } = req.body;
    User.findOne({ user }, function(err, doc) {
        if (doc) {
            return res.json({ code: 1, msg: '使用者名稱重置' })
        }
        const userModel = new User({ user, pwd: md5Pwd(pwd), type });
        userModel.save(function(e, d) {
            if (e) {
                return res.json({ code: 1, msg: '後端出錯了' })
            }
            const { user, type, _id } = d;
            res.cookie('userid', _id)
            return res.json({ code: 0, data: { user, type, _id } })
        })
    })
})
Router.get('/info', function(req, res) {
    const { userid } = req.cookies;
    if (!userid) {
        return res.json({ code: 1 })
    }
    User.findOne({ _id: userid }, _filter, function(err, doc) {
            if (err) {
                return res.json({ code: 1, msg: '後端出錯了' })
            }
            if (doc) {
                return res.json({ code: 0, data: doc })
            }
        })
        // 使用者有沒有cookie

});
// 密碼加鹽
function md5Pwd(pwd) {
    const salt = 'lhh_is_good_1310486!@#5^%~*';
    return utils.md5(utils.md5(pwd + salt))
}
module.exports = Router
複製程式碼

三、總結

本專案實現了獲取資料和表現的程式碼分離,也是對於學習React、Node和WebSocket的一次更進一步提升,當然還有很多可以改進的地方,比如可以用asyncawait進行非同步獲取資料等等。
作為一名前端菜鳥,還是希望前輩能給一些學習的建議和指點迷津
最後附上本專案的程式碼連結
github連結

相關文章