一、项目预览

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