react+react-router+redux+Node.js+socket.io写一个聊天webapp

一、项目预览

之前看一个写聊天器的教程,自己也跟着教程做了一遍,由于懒得去找图片和一些图标我就用教程中的素材来做,主要是用了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链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容