基于react+react-router+redux+socket.io+koa开发一个聊天室
最近练手开发了一个项目,是一个聊天室应用。项目虽不大,但是使用到了react, react-router, redux, socket.io,后端开发使用了koa,算是一个比较综合性的案例,很多概念和技巧在开发的过程中都有所涉及,非常有必要再来巩固一下。
项目目前部署在heroku平台上,在线演示地址: online demo, 因为是国外的平台速度可能有点慢,点进去耐心等一会儿就能加载好了。
加载好之后,首先出现的页面是让用户起一个昵称:
输入昵称之后,就会进入聊天页面,左边是进入聊天室的在线用户,右边则是聊天区域,下图是三个在线用户聊天的情形:
项目源码的github地址: 源码地址, 有兴趣的同学欢迎关注学习~
下面就来分析一下项目的整体架构,以及一下值得注意的技巧和知识点。
1. 整体结构
项目的目录如下:
├── README.md
├── node_modules
├── dist
│ ├── bundle.css
│ ├── bundle.js
│ └── resource
│ ├── background.jpeg
│ └── preview.png
├── package.json
├── server.js
├── src
│ ├── action
│ │ └── index.js
│ ├── components
│ │ ├── chatall
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── login
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── msgshow
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── namelist
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── nav
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ └── typein
│ │ ├── index.js
│ │ └── index.less
│ ├── container
│ │ ├── chatAll.js
│ │ └── login.js
│ ├── index.ejs
│ ├── index.js
│ ├── index.less
│ ├── index2.js
│ ├── reducer
│ │ └── index.js
│ ├── redux_middleware
│ │ └── index.js
│ └── resource
│ ├── background.jpeg
│ └── preview.png
└── webpack.config.js
其中src当中是前端部分的源代码。项目使用webpack进行打包,打包后的代码在dist目录当中。由于我们的项目是一个单页面应用,因此只需要统一打包出一个bundle.js和一个bundle.css。而后端使用了koa框架,由于代码相对比较少,都集中在了server.js这一个文件当中。
开发过程中,由于要webpack打包,一般我们会配合webpack-dev-server来使用。webpack-dev-server运行的时候自身就会开启一个server,而在我们的项目当中,后端koa也是一个server,因此为了简单起见,我们可以使用koa-webpack-dev-middleware来在koa当中开启webpack-dev-server。
var webpackDev = require('koa-webpack-dev-middleware');
var webpackConf = require('./webpack.config.js');
var compiler = webpack(webpackConf);
app.use(webpackDev(compiler, {
contentBase: webpackConf.output.path,
publicPath: webpackConf.output.publicPath,
hot: true
}));
2. 项目布局: flexbox实践
在这个项目中我们有意识的使用了flex布局,作为面向未来的一种新的布局方式,实践一下还是很有必要的!没有学习郭flexbox的同学可以参考这篇来学一下:http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
以聊天界面为例进行分析,使用flex布局的话,可以非常方便,下图就是对界面的一个简单的切分:
整个聊天框最外层红框框起来的部分display设置为flex,并且flex-direction设置为column,这样它里面的两个元素(即粉框和蓝框部分)就会竖直方向排列,同时粉框的flex设置为0 0 90px,代表该框不可伸缩,固定高度90px,而对于蓝框,则设置flex为1,代表伸展系数为1,这样,蓝框的高度就会占满除了粉框以外的全部空间。
而于此同时,粉框和蓝框本身又分别设置display为flex。对粉框而言,内部一共有欢迎标签和退出button两个元素,分列两侧,因此只需要设置justify-content为space-between即可做到这一点。而对蓝框而言,内部有在线用户列表以及聊天区域两个元素。这里在线用户列表(即黄色框)需要设置固定宽度,因此类似于刚才粉框的设置,flex: 0 0 240px,而聊天区域(即绿色框)则设置flex为1,这样会自适应占满剩余宽度。
最后,聊天区域内部又分为信息展示区以及打字区,因此聊天区域自身又是一个flexbox,设置方式类似,就不再具体分析了。
可以看出,使用flexbox,相比使用float以及position等等而言,更加的规整,使用这种思路,整个页面就像庖丁解牛一般,布局格外清晰。
3. 设计页面的数据结构
项目中使用了redux作为数据流管理工具,配合react,能够让页面组件同页面数据形成规律的映射。
分析我们的聊天页面,可以看出,主要的数据就是目前在线的用户昵称列表,以及消息记录,此外我们还需要记录自己的用户昵称,方便消息发送时候取用。因此,整个应用的数据结构如下, 也就是redux中的store的数据结构如下:
{
"nickName": "your nickname",
"nameList": ["user A","user B","user C","...."],
"msgList": [
{
"nickName": "some user",
"msg": "some string"
},{
"nickName": "another user",
"msg": "another string"
},
]
}
有了这个总体的数据结构,我们就可以根据该结构设计具体的action,reducer等等部分了。这里整个程序的模块拆分遵循了redux官方实例当中的拆分方法,action文件夹当中定义action creators,reducer文件夹中定义reducer函数,component文件夹中定义一些通用的组件,container文件夹当中则是将通用组件取出,定义store中的数据同组件如何映射,以及组件中的事件如何dispatch action,从而引起store数据的改变。
以component/namelist中的组件为例,该组件用于显示在线用户昵称列表,因此它接受一个数组,也就是store中的nameList作为参数,因此其通用组件的写法也很简单:
class NameList extends React.Component {
constructor(props) {
super(props);
}
render() {
var {nameList} = this.props;
return (
<ul className='name-list'>
<li className='name-list-title'>在线用户:</li>
{nameList.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)
}
}
export default NameList
而在container当中,只需要将store中的nameList赋值到该组件的props上面即可。其他组件也是类似的写法。
可以看出,在redux的思想下,我们可以对整个应用抽象出一个总体的数据结构,数据结构的改变,会引发各个组件的改变,而组件当中的各种事件,又会反过来修改数据结构,从而再次引起页面的改变,这是一种单向的数据流,总体的数据都在store这个对象中进行维护,从而让整个应用开发变得更加有规律。redux的这种程序架构是对react提出的flux架构的一种消化和改良,下图是flux架构的示意图:
4. socket.io的使用
由于是一个即时聊天应用,websocket协议自然是首选。而socket.io就是基于websocket实现的一套基于事件订阅与发布的js通信库。
在socket.io中,主要有server端和client端。创建一个server和client都非常容易,对于server端,配合koa,只需要如下代码:
var app=require('koa')();
var server = require('http').Server(app.callback());
var io = require('socket.io')(server);
client端更加简单:
var io=require('socket.io-client');
var socket = io();
一旦连接建立,client和server即可通过时间订阅与发布来彼此通信,socket.io提供的api非常类似于nodejs中的event对象的使用,对于server端:
io.on('connection',function(socket){
socket.on('some event',function(data){
//do something here....
socket.emit('another event',{some data here});
});
});
对于client端,同样通过socket.on以及socket.emit来订阅和发布事件。比如说,某一个client端口emit了event A,而如果server端口订阅了event A,那么在server端,对应的回调函数就会被执行。通过这种方式,可以方便的编写即时通信程序。
5. 一些值得注意的实现细节
下面对程序中涉及的一些我认为值得注意的细节和技巧进行一下简要分析。
1. socket.io同redux的结合方案:redux中间件的运用
在程序编写过程当中,我遇到一个难题,就是如何将socket.io的client实例结合到redux当中。
socket.io的client类似于一个全局的对象,它不属于任何一个react组件,它订阅到的任何消息都可能更改整个应用的数据结构,而这种更改在redux当中又只能通过dispatch来实现。思考之后,我觉得编写一个redux中间件来处理socket.io相关的事件是一个很好的选择。
关于redux中间件,简单来说,就是在redux真正出发dispatch之前,中间件可以首先捕获到react组件出发的action,并针对不同action做一些处理,然后再调用dispatch。中间件的写法,在redux的官方文档当中写的非常详细,有兴趣的可以参考一下: http://redux.js.org/docs/advanced/Middleware.html , 后续我也会出一些系列文章,深入分析redux包括react-redux的原理,其中就会提到中间件的原理,尽请期待~
知道了redux中间件是怎么一回事之后,我们就可以发现,socket.io相关的事件非常适合通过写一个中间件来处理。我们程序当中中间件如下所示:
import { message_update, guest_update } from '../action'
function createSocketMiddleware(socket) {
var eventFlag = false;
return store => next => action => {
//如果中间件第一次被调用,则首先绑定一些socket订阅事件
if (!eventFlag) {
eventFlag = true;
socket.on('guest update', function(data) {
next(guest_update(data));
});
socket.on('msg from server', function(data) {
next(message_update(data));
});
socket.on('self logout', function() {
window.location.reload();
});
setInterval(function() {
socket.emit('heart beat');
}, 10000);
}
//捕获action,如果是和发送相关的事件,则调用socket对应的发布函数
if (action.type == 'MSG_UPDATE') {
socket.emit('msg from client', action.msg);
} else if (action.type == 'NICKNAME_GET') {
socket.emit('guest come', action.nickName);
} else if (action.type == 'NICKNAME_FORGET') {
socket.emit('guest leave', store.getState().nickName);
}
return next(action);
}
}
export default createSocketMiddleware
这段代码是一个socket middleware的创建函数,从中我们可以看出,这个中间件如果第一次调用的话(eventFlag),会首先绑定一些订阅主题和对应的回调函数,主要是订阅了消息到达、新用户来到、用户离开等等事件。同时,中间件会在真正dispatch函数调用之前,首先捕获action,然后分析action的type。如果是和发送事件相关的,就会调用socket.emit来发布对应的事件和数据。比如说,在我们的应用中,点击“发送”按钮会触发一个type为"MSG_UPDATE"的事件,这个事件首先被中间件捕获,那么这时候就会出发socket.emit('msg from client')来将消息发送给server。
2. 权限验证: 单页面应用中的页面跳转
整个应用使用react-router,做成了一个单页面应用,其中前端路由的层级非常简单:
render(
<Provider store={store}>
<Router history={hashHistory}>
<Route path='/' component={ChatAllContainer}/>
<Route path='/login' component={LoginContainer}/>
</Router>
</Provider>
,
document.getElementById('test'));
可以看出,主要是两条路径: '/'和'/login',其中'/'是我们的聊天界面,而'/login'则是起昵称界面。
由于应用的逻辑是,只有用户起了昵称才可以进入聊天界面,因此我们需要做一些权限验证,对于没有起昵称就进入'/'路径的用户,需要跳转到'/login'。在传统多页面web应用中,我们对于跳转非常熟悉,无非是服务器发送一个重定向请求,浏览器就会重定向到新的页面。然而在单页面中,由于始终只有一页,服务器又能够让浏览器跳转到哪里去呢?也就是说,服务器重定向的方法是行不通的。
因此,我们换一种思路,页面跳转的逻辑需要在浏览器端执行,在react-router的框架下,执行跳转也非常简单,只需要使用其中的hashHistory对象,通过hashHistory.push('path'),即可让应用跳转到指定路径对应的界面。有了这个认知,那么我们下面要解决的,就是何时控制单页面的跳转?
我的思路是,将用户的昵称通过一定的加密和编码,保存在cookie当中。当用户访问'/'的时候,在对于界面的组件挂载之前,首先会向服务器发送一个认证请求,服务器会从请求中读取cookie,如果cookie当中没有用户名存在,那么服务器返回的参数当中有一个'permit'字段,设置为false,当应用解析到该字段后,就会调用hashHistory.push('/login')来让页面跳转到起昵称界面下。这部分对应的逻辑主要在container/chatAll.js文件当中实现。
3. 文本输入的细节处理: xss的预防,以及组合键的识别
在我们的聊天应用中,如果不对用户的输入进行一些处理,就有可能导致xss漏洞。举个例子,比如说有一个用户输入了'',如果不进行一些防范,输入到消息显示界面,这段文字就直接被解析成为了一段js代码。为了防范这类攻击,这里我们需要做一些简单的预防:
var regLeft = /</g;
var regRight = />/g;
value = value.replace(regLeft, '<');
value = value.replace(regRight, '>');
这段代码在components/typein组件当中。
此外,为了方便用户快速发送消息,在消息输入框中,我们设置了'enter'按键为之间发送按键。那么,为了让用户能够打出换行,我们模仿微信,约定用户输入ctrl+enter组合键的时候是换行,这样,在消息输入框中,就需要监听组合键。在js的键盘事件中,event对象有一个ctrlKey属性,用于判断ctrl按键是否按下:
someDom.onkeydown=function(e){
if(e.keyCode==13&&e.ctrlKey){
//组合键被按下
}
}
这就是组合键监听的原理。
以上就是对于这个项目的概述以及一些细节的讲解。最后安利一下我的博客 http://mly-zju.github.io/,会不定期更新我的原创技术文章和学习感悟,欢迎大家关注~
基于react+react-router+redux+socket.io+koa开发一个聊天室的更多相关文章
- html5的新通讯技术socket.io,实现一个聊天室
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 利用socket.io+nodejs打造简单聊天室
代码地址如下:http://www.demodashi.com/demo/11579.html 界面展示: 首先展示demo的结果界面,只是简单消息的发送和接收,包括发送文字和发送图片. ws说明: ...
- 基于Node.js+socket.IO创建的Web聊天室
这段时间进了一个新的项目组,项目是用Appcan来做一个跨平台的移动运维系统,其中前台和后台之间本来是打算用WebSocket来实现的,但写好了示例后发现android不支持WebSocket,大为受 ...
- 利用socket.io实现多人聊天室(基于Nodejs)
socket.io简单介绍 在Html5中存在着这种一个新特性.引入了websocket,关于websocket的内部实现原理能够看这篇文章.这篇文章讲述了websocket无到有,依据协议,分析数据 ...
- 利用socket.io构建一个聊天室
利用socket.io来构建一个聊天室,输入自己的id和消息,所有的访问用户都可以看到,类似于群聊. socket.io 这里只用来做一个简单的聊天室,官网也有例子,很容易就做出来了.其实主要用的东西 ...
- 基于 nodejs 的 webSockt (socket.io)
基于 nodejs 的 webSockt (socket.io) 理解 本文的业务基础是在基于 nodejs 的 socket.io 的直播间聊天室(IM)应用来的. 项目中具体的 框架如下 expr ...
- 以C#编写的Socket服务器的Android手机聊天室Demo
内容摘要 1.程序架构 2.通信协议 3.服务器源代码 4.客户端源代码 5.运行效果 一.程序架构 在开发一个聊天室程序时,我们可以使用Socket.Remoting.WCF这些具有双向通信的协议或 ...
- AngularJS+Node.js+socket.io 开发在线聊天室
所有文章搬运自我的个人主页:sheilasun.me 不得不说,上手AngularJS比我想象得难多了,把官网提供的PhoneCat例子看完,又跑到慕课网把大漠穷秋的AngularJS实战系列看了一遍 ...
- 使用socket.io client 开发时兼容IE低版本的办法
使用socket.io client 开发时兼容IE低版本的办法 socket.io提供了针对各个版本浏览器的‘socket’功能的封转:websocket,长连接,流,flash什么的.给你格式化下 ...
随机推荐
- 让php Session 存入 redis 配置方法
首先要做的就是安装redis 安装方法:http://redis.io/download Installation Download, extract and compile Redis with: ...
- 添加redo日志组和添加日志组多元化
查看redo日志组的状态和日志的位置. SQL> 没有被使用,所以切几次日志,组合4已生效. SQL> select * from v$log; GROUP# THREAD# SEQ ...
- PHP 文件上传全攻略
PHP文件上传功能一般都是大家使用事先封装好的函数,要用的时候直接使用已封装的函数就完了,但有时候不能使用封装函数,还真不大能记住PHP的上传相关的东西,在此做个总结,以备后用. 1.表单部分 允 ...
- 关于MVC结构
简单的记录,只是想记录一下现在对MVC的理解. MVC,即模型(MODEL),视图(VIEW),控制器(CONTROLLER) 模型是数据模型 视图是图形界面 控制器是在两个之间的控制部分,用来将数据 ...
- 关于我的PP0.1聊天软件(客户端)
登陆界面: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data ...
- JS如何实现点击页面其他地方隐藏菜单?
方法一: $("#a").on("click", function(e){ $("#menu").show(); $(documen ...
- oracle sql 知识小结
Oracle_sql : 第一单元:select 语句: ①:字符串连接操作符: || ②:去除重复行:distinct 第二单元:条件限制和排序 ①:关键字:where ②:比较操作符:=,&g ...
- 使用express.js框架一步步实现基本应用以及构建可扩展的web应用
最近过年在家有点懈怠,但是自己也不断在学习新的前端技术,在家琢磨了express.js的web框架. 框架的作用就是提高开发效率,快速产出结果.即使不使用框架,我们也会在开发过程中逐渐形成构成框架. ...
- Unity预计算光照的学习(速度优化,LightProb,LPPV)
1.前言 写这篇文章一方面是因为unity的微博最近出了关于预计算光照相关的翻译文章,另一方面一些美术朋友一直在抱怨烘培速度慢 所以抱着好奇的心态来学习一下unity5的PRGI预计算实时光照 2.基 ...
- SQL SPLIT2
CREATE FUNCTION F_SQLSERVER_SPLIT( @Long_str varchar ( 8000 ), @split_str varchar ( 100 )) ...