本应用使用 Create React App 初始化
npm run start
npm run build
- 登陆功能
- 求职者登陆
- 老板登陆
- 注册功能,需要输入账号信息以及选择头像和填写个人信息,比如用用户类型
- 注销功能
- 查看职位信息功能(求职者)
- 查看求职信息功能(老版)
- 聊天功能:双方实时聊天,并且可以发送表情
- 历史消息记录:保留上次聊天的对象的消息,且将最新消息置顶
- 查看个人信息功能
- 基于前后台分离的招聘的单页应用
- 包括用户注册,登陆,求职者和招聘者的列表,实时聊天功能
- 前端技术栈:使用React 全家桶 以及 Ant Design UI
- 后端技术栈:基于NodeJs的服务端,使用MongoDB数据库
- 使用模块化、组件化、工程化的模式进行开发
- 前端部分(模块化):React + react-router + redux + Ant Design UI
- 后台部分:NodeJs + express + MongoDB + mongoose + SocketIO
- 前后台交互
- ajax请求部分:axios ,
- 交互模式:async await
- 测试接口:postman
- 模块化:ES6 Babel
- 工程化项目构建:webpack + react脚手架 + eslint代码风格检查
- 辅助库:js-cookie blueimp-md5等
- src
- api ajax请求相关的模块文件夹
- assets 静态资源文件夹
- components UI组件封装的模块文件夹
- containers 容器组件模块文件夹(在此组装UI组件)
- redux 状态管理相关模块
- utils 工具类相关模块(md5 cookie socket.io等)
- index.js webpack打包入口js文件
| 功能 | 相对路径(/) | 组件(.jsx) |
|---|---|---|
| 注册 | /register | register |
| 登陆 | /login | login |
| 通用界面 | / | main |
| 老版主界面 | /boss | boss |
| 求职者主界面 | /jobseeker | jobseeker |
| 消息列表界面 | /message | message |
| 个人中心界面 | /personal | personal |
| 老板信息完善界面 | /boss-info | boss-info |
| 求职者信息完善界面 | /jobseeker-info | jobseeker-info |
| 聊天界面 | /chat/:userid | chat |
| 404页面 | /not-found | not-found |
用户数据Schema: users
| 字段名 | 字段类型 | 是否必要 |
|---|---|---|
| username | String | 是 |
| password | String | 是 |
| useType | String | 否 |
| avatar | String | 否 |
| position | String | 否 |
| personalInfo | String | 否 |
| companyInfo | String | 否 |
| salary | String | 否 |
聊天数据Schema: chat
| 字段名 | 字段类型 | 是否必要 |
|---|---|---|
| from_id | String | 是 |
| to_id | String | 是 |
| chat_id | String | 是 |
| content | String | 是 |
| hasRead | Boolean | 默认为false |
| create_time | Number | 否 |
- from_id 发送者id
- to_id 接受者id
- chat_id 聊天会话id,由发送接受者共同决定
- content 聊天内容
- hasRead 对方是否已读
- create_time 创建时间,排序使用
-
UI 均使用antd-mobile组件进行构建
-
登陆页面
- 必要字段信息
- 用户名:手机号、邮箱、和8-12为的字符串构成
- 密码:就是密码
- 按钮:
- 登陆:发送请求验证身份,并进入主页面
- 注册账户:跳转到注册页面
- 附加项:
- 登陆cookie持久化,维持登陆一天
- 必要字段信息
-
注册页面
- 必要字段信息
- 用户名:手机号、邮箱、或者8-12为的字符串构成
- 密码:6-12位的字符串构成,需要至少包含一个大写或小写字母和数字
- 确认密码
- 用户类型:单向按钮,选择求职者和老板其中一个,默认为求职者
- 按钮
- 注册:发送请求并添加账户信息,并进入主页面
- 登陆:跳转到登陆页面
- 必要字段信息
-
用户信息完善页面
// src/api/ajax.js
import axios from 'axios'
export default function ajax (method = 'GET', url = '', data = {}) {
if (method === 'GET') {
return axios.get(url, {
params: data
})
}
if (method === 'POST') {
return axios.post(url, data)
}
}
/* src/api/index.js
* 网络接口模块
* 返回值均为Promise对象*/
import ajax from './ajax'
const requestRegister = user => ajax('POST', '/register', user)
const requestLogin = user => ajax('POST', '/login', user)
const requestUserUpdate = userInfo => ajax('POST', '/update-user-data', userInfo)
const requestUserData = () => ajax('GET', '/get-user-data')
const requestUserList = (userType) => ajax('GET', '/get-user-list', { userType })
const requestUserMsgData = () => ajax('GET','/get-msg-list')
const reqMsgHasRead = (from_id) => ajax('POST', '/has-read-msg', { from_id })
export {
requestRegister,
requestLogin,
requestUserUpdate,
requestUserData,
requestUserList,
requestUserMsgData,
reqMsgHasRead
}-
下载按需打包模块
npm i --save-dev babel-plugin-import react-app-rewired打包报错解决方案:https://blog.csdn.net/zoepriselife316/article/details/88063171
-
下载less-loader以实现自定义样式,因为
antd的样式使用less书写-
css-loader出错解决方案:
addLessLoader({ lessOptions:{ javascriptEnabled:true , ModifyVars:{ '@primary-color':'#eee' } } })
-
-
实现对antd mobile 的样式按需打包
./config-overrides.js const { override, fixBabelImports, addLessLoader, addWebpackAlias } = require('customize-cra') const path = require('path') const theme = require('./custom-antd-theme') function resolve (dir) { return path.join(__dirname, '.', dir) } module.exports = override( fixBabelImports('import', { libraryName: 'antd-mobile', libraryDirectory: 'es', style: true }), addLessLoader({ lessOptions:{ // 新版需要使用lessOptions进行包裹 javascriptEnabled: true, modifyVars: theme } }), addWebpackAlias({ '@': resolve('src') }), )
socket.io是一个能实现多人实时聊天的库,同时包装H5的WebSocket和轮询机制,来保证兼容性
由于客户端涉及socketIO的代码量稀少,这里就没有将socketIO进行封装使用,而是直接在异步actions中使用单例模式初始化,将产生连接的实例挂到全局io对象的属性上,避免页面刷新导致的Socket实例重复创建
// src/redux/actions.js
import io from 'socket.io-client'
// ...
const initWebSocket = (dispatch) => {
if (!io.webSocket) {
console.log('初始化WebSocket')
io.webSocket = io('ws://47.115.62.216:5000')
io.webSocket.on('serverMsg', data => {
dispatch(receiveSingleMsg(data))
})
}
}使用代理解决:在package.json中添加代理
"proxy": "http://localhost:5000"-
集中管理action-types
/* * 包含多个action类型常量 * */ const USER_AUTH_SUCCESS = 'USER_AUTH_SUCCESS' const USER_UPDATE = 'USER_UPDATE' const USER_STATUS_RESET = 'USER_UPDATE_ERR' const ERROR_MSG = 'ERROR_MSG' const SENT_REQ = 'SENT_REQUEST' const RCVD_RES = 'RECEIVED_RESPONSE' const USER_LIST = 'USERS_LIST' const MESSAGE_DATA = 'MESSAGE_LIST' const RECEIVE_SINGLE_MSG = 'RECEIVE_SINGLE_MSG' const MESSAGE_HAS_READ = 'MESSAGE_HAS_READ' export { USER_AUTH_SUCCESS, ERROR_MSG, SENT_REQ, RCVD_RES, USER_UPDATE, USER_STATUS_RESET, USER_LIST, MESSAGE_DATA, RECEIVE_SINGLE_MSG, MESSAGE_HAS_READ }
-
初始化用户信息的状态
- msg:存储服务器响应的错误、提示信息
- redirectTo:存储路由生成模块返回的路由信息
- isRequesting:请求状态标识,用于加载动画的显示与销毁
const initUser = { username: '', userType: '', // 用户类型 msg: '', // 如果出错,存储错误信息, redirectTo: '', isRequesting: false, }
-
初始化聊天信息的状态,
- users:保存当前所有用户的头像信息与名称
- chatMessages:存储于登陆用户有关的所有聊天信息
- unreadMsgcount:初始化信息的未读数量
const initChatData = { users: {}, // 属性名:用户id,属性值用户姓名,头像信息 chatMessages: [], unreadMsgcount: 0 }
-
在渲染主路径以
/之前先判断当前浏览器cookie是否拥有userId- 如果
userId不存在,则直接跳转到登陆页面/login - 如果
userId存在,再判断redux中是否存在_id- 如果
_id存在,检查对应userType以及检查是否存在头像信息avatar- 如果请求的是根路径
/,则计算跳转的默认路由- 如果
avatar存在,则直接渲染对应用户主界面 - 如果
avatar不存在,通过userType判断得到redirectTo重定向路由信息,跳转到对应用户的信息完善页面
- 如果
- 如果请求的是根路径
- 如果
_id不存在,则说明网页被刷新,则使用cookie向·服务器发送请求获取用户数据,渲染加载中页面
- 如果
- 如果
-
注册
/register、登陆/login、主页面/ReactDOM.render(( <Provider store={store}> <HashRouter> <Switch> <Route path='/register' component={Register}/> <Route path='/login' component={Login}/> <Route path='/' component={Main}/> {/*默认组件*/} </Switch> </HashRouter> </Provider> ), document.getElementById('root') )
-
路由生成模块
/* src\utils\redirect-route\index.js * 在用户成功被授权后,路由拥有4种状态 * 用户主界面: * /boss * /jobseeker * 用户信息完善页面: * /boss-info * /jobseeker-info * 判断是否已经完善信息: user.avatar * 如果头像信息不存在,用户信息完善页面 * 判断用户类型:user.type * */ function getRedirecUrl (userType, avatar) { let path if(userType === 'boss'){ path = '/boss' } if(userType === 'jobseeker') { path = '/jobseeker' } if(!avatar){ path += '-info' } return path } export {getRedirecUrl}
-
注册/登陆页面:通过表单验证后,调用redux中actions.js的异步action函数调用网络模块进行发送异步请求,在异步action中,使用async 函数配合await达到等待Promise返回的同步效果,并且简化了响应消息的接收方式
// 同步action const userAuthSuccess = (user) => ({ type: USER_AUTH_SUCCESS, data: user }) // 异步action返回一个函数,在此使用ES8新增的async const register = (user) => { return async dispatch => { /* 传统方法 const promise = registerRequest(user) promise.then(response => { const result = response.data }) */ // 在此发送ajax请求 dispatch(sentReq()) // response 直接得到响应的data值 const response = await requestRegister(user) const responseData = response.data if (responseData.code === 0) { // 注册成功,携带响应信息发布一个同步action到reducer中 dispatch(userAuthSuccess(responseData.data)) } else { // 注册失败,需要得到错误信息msg dispatch(errorMsg(responseData.msg)) } dispatch(receivedRes()) } }
-
注册成功:携带响应信息发布一个同步action到reducer中,并计算路由,此时浏览器得到服务器响应的一个cookie
-
注册失败:返回错误信息
const user = (state = initUser, action) => { switch (action.type) { //... case USER_AUTH_SUCCESS: const { userType, avatar } = action.data // 使用解构的目的是为了保留state原来的某些属性不被action数据所覆盖 return { ...action.data, redirectTo: getRedirecUrl(userType, avatar) } //... default : return state } }
当redux状态更新后,触发注册组件重新渲染,在渲染之前得到了redirectTo的值并且
react-router的Redirect组件使用进行页面的跳转到对应信息完善页面// render () { const { userType } = this.state const {msg,redirectTo} = this.props.user // redirectTo 有值,则重定向,这个值是用户通过注册得到的 if(redirectTo){ return <Redirect to={redirectTo} /> } return ( // ..... ) } export default connect( // 连接 state => ({ user: state.user }), { register } )(Register)
在信息完善页面与注册页面同理,区别在于对服务器返回的avatar字段信息进行非空判断,再跳转到对应页面
render () { //导航守卫 const { userType, avatar } = this.props.user // 如果redux中有用户头像数据,则直接进入用户主页面 if (avatar) { const redirectpath = getRedirecUrl(userType,avatar) return <Redirect to={redirectpath}/> } return (//...) }
-
自动登录
当在刷新页面后,redux中的信息会全部丢失,但cookie保存在浏览器本地,此时需要使用另一个接口来实现自动登录的功能
在除了/register 和 /login 之外的路由均为main组件的子路由,所以在子路由上执行刷新操作时,在main组件挂载后,会根据cookie发送一个获取用户信息的请求
// src/main/main.jsx //在挂载时如果cookie中userId存在,且redux中_id不存在,说明刷新过,此时发送请求 componentDidMount () { const userId = Cookies.get('userId') console.log('componentDidMount:userId:',userId) const { _id } = this.props.user if (userId && !_id) { // 发送登陆请求 this.props.getUserData() } } //... // 在渲染时,再次判断userid是否存在于cookie中 render () { //导航守卫 const userId = Cookies.get('userId') //如果cookie中userId不存在,重定向到登陆页面 if (!userId) { return <Redirect to={'/login'}/> } else { const { user } = this.props //如果Redux中_id不存在,不显示任何数据 // 此处的判断是为了等待 ajax请求结果 if (!user._id) { return null } else { let path = this.props.location.pathname if (path === '/') { // 如果请求的根路径,则检查信息是否完善,再跳转 path = getRedirecUrl(user.userType, user.avatar) return <Redirect to={path}/> } } } }
-
主页面导航
-
采用路由信息渲染不同用户的不同页面,但在大型应用中,路由信息列表一般由服务器返回
-
路由信息存储在数组中,并分为2个不同类型的路由信息
- 公共路由
- 私有路由
navList = [ { path: '/boss', component: Boss, title: '求职者列表', icon: 'jobseeker', text: '求职列表' }, { path: '/jobseeker', component: Jobseeker, title: '职位列表', icon: 'job', text: '职位列表' } ] publicNavList = [ { path: '/message', component: Message, title: '消息管理', icon: 'message', text: '消息管理' }, { path: '/personal', component: Personal, title: '个人中心', icon: 'person', text: '个人中心' }, ]
-
通过判断,将私有路由与公共路由组合到一起,再声明到react的路由中,值得注意的是,底部导航栏需要通过当前路径获得的路由信息来判断是否显示,否则切换到聊天页面时,导航栏不会隐藏
render () { // ... // 获得路由列表 const { navList, publicNavList } = this // 获得redux中的user状态 const { user } = this.props // 获得当前所在页面的路径 let path = this.props.location.pathname // 拿到当前所在页面的路由信息 const currentNavInfo = [...navList, ...publicNavList].find(item => item.path === path) // 当前用户类型所对应的导航栏的路由列表 const customNavTabs = [ navList.find(item => item.path === `/${user.userType}`), ...publicNavList ] return ( <div> // 渲染标题栏 {currentNavInfo ? <NavBar className="sticky-header">{currentNavInfo.title}</NavBar> : null} <Switch> // 按用户类型组件注册不同的react路由 { [...navList, ...publicNavList].map(item => ( <Route key={item.path} path={item.path} component={item.component}/> )) } <Route path='/boss-info' component={BossInfo}/> <Route path='/jobseeker-info' component={JobseekerInfo}/> <Route path='/chat/:userId' component={Chat}/> <Route component={NotFound}/> </Switch> // 渲染导航栏 {currentNavTabs ? <NavTabs unreadMsgcount={this.props.unreadMsgcount} customNavTabs={customNavtabs}/> : null} </div> ) }
-
接口
- 连接服务器SocketIO
- 即时接受服务器信息
- 用户发送消息
- 获得用户所有聊天消息
-
确认未读消息
-
使用同步action函数,通过单例模式初始化SocketIO,
-
监听服务器socketIO的
serverMsg事件,获取即时消息,并将获得的消息通过dispatch对应action更新状态,并改变未读消息数量// actions中 // 同步action const receiveSingleMsg = (message) => ({ type: RECEIVE_SINGLE_MSG, data: message }) // 初始化SocketIO const initWebSocket = (dispatch) => { if (!io.webSocket) { io.webSocket = io('ws://xxxxxxx:5000') // 监听服务器的消息 io.webSocket.on('serverMsg', data => { dispatch(receiveSingleMsg(data)) }) } } // reducer中 const chat = (state = initChatData, action) => { const userId = Cookies.get('userId') switch (action.type) { //... case RECEIVE_SINGLE_MSG: return { users: state.users, chatMessages: [...state.chatMessages, action.data], // 如果接受的消息中hasRead标志为false 并且 信息是对方发给我的消息,则将未读数量加1 unreadMsgcount: state.unreadMsgcount + (!action.data.hasRead && action.data.to_id === userId ? 1 : 0) } //... default: return state } }
-
初始化SocketIO的时机:向服务器请求聊天信息时,在异步action中进行调用
-
发送获取聊天信息请求的时机:(自动)登陆、提交信息完善,并接收到成功的响应之后
const getMsgList = async (dispatch) => { // 初始化SocketIO,并传入其需要的dispatch initWebSocket(dispatch) const response = await requestUserMsgData() const responseData = response.data if (responseData.code === 0) { const { users, chatMessages } = responseData.data dispatch(getMessageData({ users, chatMessages })) } }
-
发送即时聊天信息时机:点击发送按钮、按下enter键时,使用异步action通过socketIO实例向服务器发布事件clientMsg
const sendMessage = (from_id, to_id, content) => { // 不需要要使用ajax发送请求,使用socket发送 return () => { io.webSocket.emit('clientMsg', { from_id, to_id, content }) } }
-
确认未读消息的时机:进入、离开聊天页面时,在聊天前后的信息均可以被确认
const hasReadMessage = (targetId) => { return async dispatch => { const response = await reqMsgHasRead(targetId) const responseData = response.data if (responseData.code === 0) { const count = responseData.data dispatch(messageHasRead(count, targetId)) } } }
-
获取用户与相关用户的最后一条聊天信息:处理chatMessages
-
创建一个对象
latestMessagesObj,遍历chatMessages数组,也就是单个元素为上图对象的数组,将每个chat_id创建为这个对象的一个属性,通过对比相同chat_id中的create_time字段,选择最后一个chat_id的信息 -
然后使用for in 对对象的key进行遍历,将
latestMessagesObj对象中每个chat_id中的内容推入数组中,再使用sort以内部最后一条信息的create_time来进行排序,得到接收消息时间降序的数组const getLatestMsg = (chatMessages, user_id) => { let latestMessagesObj = {} let latestMessagesArray = [] chatMessages.forEach(msg => { const chatId = msg.chat_id // 初始化定义对象属性,如果没有对应属性则使用Object.defineProperty对齐进行添加,可枚举与可写一定要声明为true,不然无法遍历和更改 if (!latestMessagesObj[chatId]) { Object.defineProperty(latestMessagesObj, chatId, { value: {}, writable: true, enumerable: true }) // 定义以chat_id为属性名的属性内部的三个属性 Object.defineProperties(latestMessagesObj[chatId], { latestMsg: { // 存放一条完整的聊天信息 value: msg, writable: true, enumerable: true }, unreadCount: { // 存放未读数量,初始化时需要判断 value: !msg.hasRead && msg.to_id === user_id ? 1 : 0, writable: true, enumerable: true, }, targetId: { // 存放对方的userId value: msg.to_id !== user_id ? msg.to_id : msg.from_id, enumerable: true } }) } else { // 在已经初始化了上述对象之后,默认执行以下语句 // 对比消息时间,把最新的消息信息放入latestMsg的属性中 if (msg.create_time > latestMessagesObj[chatId].latestMsg.create_time) { latestMessagesObj[chatId].latestMsg = msg } // 统计未读消息数量 if (!msg.hasRead && msg.to_id === user_id) { latestMessagesObj[chatId].unreadCount++ } } }) // 将上述的对象的属性抛弃chat_id转换为数组元素,并通过创建时间进行排序 for (let key in latestMessagesObj) { latestMessagesArray.push(latestMessagesObj[key]) } return latestMessagesArray.sort((a, b) => b.latestMsg.create_time - a.latestMsg.create_time) }
-
-
src
- bin http服务模块文件夹
- db mongoose的module模块,连接数据库
- public 静态资源文件夹
- router 路由模块,提供接口
- socket-io socketIO模块
- app.js 服务器入口模块
-
端口:5000
-
用户注册接口
-
路径:/register
-
请求方式:POST
-
接收参数:
- username
- password ,
- userType, administrator为管理员用户名
-
注册成功:返回
{code:0,data:{ _id:'xxx', username:'xxx'}}
-
注册失败:返回
{code:1,msg: '用户名已存在,或不可使用'}
-
-
用户登陆接口
-
路径:/login
-
请求方式:POST
-
接收参数:
- username
- password
-
登陆成功:返回
{code:0,data:userDoc}
-
登陆失败:返回
{code:1,msg:'用户名或密码错误'}
-
-
更新用户信息接口
-
路径:/update-user-data
-
请求方式:POST
-
接收参数:
- name 真实姓名
- avatar 头像信息
- position 职位信息(老板)
- personalInfo 职位要求、个人简介
- companyInfo 公司信息 (老板)
- salary 薪资信息(老板)
-
cookie不存在,返回
{code:1,msg:'请先登录'}
-
更新成功:
{code:0,data:user}
-
id查找到的信息不存在,更新失败,返回
{code:1,msg:'请先登录'}
-
-
获取用户信息接口(自动登录)
-
路径:/get-user-data
-
请求方式:GET
-
接收参数:
- userId cookie中的userId
-
找不到userId 对应的用户信息,返回
{code:1,msg:'请先登录'}
-
找到了,并且返回userId对应的用户信息
{code:0,data:user}
-
-
获取用户列表
-
路径:/get-user-list
-
请求方式:GET
-
接收参数:
- userType
-
没有信息返回,返回:
{code:1,msg:'当前无内容'}
-
寻找到信息,返回:
{code:0,data:usersDoc}
-
-
聊天接口
-
获取当前用户所有聊天信息列表
- 路径:/get-msg-list
- 请求方式:GET
- 接收参数:无
- 返回:
- 所有用户的id,姓名以及头像信息,因为不能确定向某个用户发送信息,使用用户的id作为对象属性名
-
当前用户与某个用户的聊天信息数据结构,chatMessages 为数组类型,内部包含多个聊天信息对象
 -
修改指定消息为已读
-
路径:/has-read-msg
-
请求方式:POST
-
接收参数:from_id
- from_id 当前消息列表中对方的id
-
在点击未读消息时,客户端向服务器发起当前请求,修改
-
返回:
-
{code: 0,data: msgDoc.nModified // .nModified返回修改的条目总数}
-
-
socket.io接口
- url: ws://xxxxxxx:5000
-
引入mongoose,连接到MongoDB
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/test',
{ useNewUrlParser: true, useUnifiedTopology: true })
const mongooseConnection = mongoose.connection
mongooseConnection.on('connected', () => {
console.log('数据库连接成功!!')
})定义一个用户身份信息的约束文档结构Schema,以及聊天信息约束文档结构Schema
const userSchema = mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true },
userType: { type: String, required: true },
name: { type: String },
avatar: { type: String },
position: { type: String }, // 职位信息
personalInfo: { type: String }, // 个人信息、职位要求
companyInfo: { type: String },
salary: { type: String }
})
const chatSchema = mongoose.Schema({
from_id: { type: String, required: true }, // 消息的发起者的用户id
to_id: { type: String, required: true }, // 消息的接受者的用户id
chat_id: { type: String, required: true }, // 消息的聊天室id
content: { type: String, required: true },
hasRead: { type: Boolean, default: false },
create_time: { type: String },
})定义用于操作集合的Model,其本质为构造函数
const UserModel = mongoose.model('users', userSchema)
const ChatModel = mongoose.model('chats', chatSchema)由于初次入门SocketIO,没有SocketIO的room深入研究,这里使用用户连接服务器时携带的cookie进行用户在线检测,并向特定用户发送实时聊天信息
module.exports = (server) => {
const io = require('socket.io')(server)
//io对象监听连接,每有一个连接建立,即创建一个socket
io.on('connection', socket => {
console.log('client connected')
// 监听客户端信息,在此处每有一个客户端连接,就会创建一个socket对象
socket.on('clientMsg', ({ from_id, to_id, content }) => {
console.log('收到客户端:', from_id, '的消息')
// 保存客户端消息到数据库
const chat_id = [from_id, to_id].sort().join('_')
const create_time = Date.now()
new ChatModel({ from_id, to_id, content, chat_id, create_time })
.save((err, chatMsg) => {
// 向客户端返回消息
socket.emit('serverMsg', chatMsg)
// 通过请求中的to_id,去获得模板客户端的socketId
let toSocketId = getSocketIdFromCookie(to_id, io)
console.log(toSocketId)
// 如果对方socketId存在,则说明对方在线
if (toSocketId) {
io.sockets.connected[toSocketId].emit('serverMsg', chatMsg)
console.log('向客户端', from_id, '发送消息')
}
})
})
})
}
// 通过消息接受者的用户id 获取对应的socketId
const getSocketIdFromCookie = (to_id, io) => {
// 在io.sockets.connected对象中可获取到所有已经连接的客户端的信息,其每个属性以socketId标识
const connectedClient = io.sockets.connected
// 将所有socketId保存到数组中
const connectedClientSocketIds = Object.keys(connectedClient)
// 遍历每个属性内的内容,这里主要是找到对应对象中的cookies,但cookies是以字符串形式保存
for (let key of connectedClientSocketIds) {
if (connectedClient[key].handshake.headers.cookie) {
// 尝试匹配到userId=******** ,如果匹配到,则使用正则进行去除,由此拿到目标的userId
let targetCookies = connectedClient[key].handshake.headers.cookie.match(/userId=[\w|\s]*/)
let targetId = targetCookies ? targetCookies.toString().replace(/userId=/g,''): null
// 如果扫描到的userId与聊天信息中的to_id相同,则返回这个SocketId
if (targetId === to_id) {
return connectedClient[key].id
}
}
}
return false
}- 前端部分
- 基本熟悉了一个比较完整的开发流程
- 体验了一次工程化、组件化、模块化的开发模式,尽管在配置按需打包时,遇到一些问题,而且对webpack并不是很熟悉
- 掌握使用react-create-app脚手架初始化项目
- 学会使用react全家桶的SPA的开发,包括路由和状态管理
- 学会使用react第三方UI组件库 Ant Design Mobile构建页面
- 基本用axios与后台进行数据交互
- 学会使用 socket.io 库实现实时通信,但实现的方式有许多不妥以及不稳定之处,需要继续对socket.io进行探索
- 后台部分
- 熟悉了使用express框架以及mongoose进行NodeJs的后台开发
- 学会使用express搭建后台路由,编写接口
- 通过查阅文档,学会使用基本的mongoose来操作MongoDB数据库
存在问题
- 服务器的接口编写不严密,错误的数据请求可能会让服务器进程崩溃












