一、项目预览
之前看一个写聊天器的教程,自己也跟着教程做了一遍,由于懒得去找图片和一些图标我就用教程中的素材来做,主要是用了 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
来代表这个聊天室的唯一性,可以用 from
和to
拼接,拼接函数写在 util.js
中。
3.Server
后端接口用到了 node.js
的express
框架,数据库用到了 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
的一次更进一步提升,当然还有很多可以改进的地方,比如可以用 async
和await
进行异步获取数据等等。
作为一名前端菜鸟,还是希望前辈能给一些学习的建议和指点迷津
最后附上本项目的代码链接
github 链接