最近练手开发了一个项目,是一个聊天室应用。项目虽不大,可是使用到了react, react-router, redux, socket.io,后端开发使用了koa,算是一个比较综合性的案例,不少概念和技巧在开发的过程当中都有所涉及,很是有必要再来巩固一下。javascript
项目目前部署在heroku平台上,在线演示地址: online demo, 由于是国外的平台速度可能有点慢,点进去耐心等一下子就能加载好了。css
加载好以后,首先出现的页面是让用户起一个昵称:html
输入昵称以后,就会进入聊天页面,左边是进入聊天室的在线用户,右边则是聊天区域,下图是三个在线用户聊天的情形:前端
项目源码的github地址: 源码地址, 有兴趣的同窗欢迎关注学习~java
下面就来分析一下项目的总体架构,以及一下值得注意的技巧和知识点。node
项目的目录以下:react
├── 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打包,通常咱们会配合webpack-dev-server来使用。webpack-dev-server运行的时候自身就会开启一个server,而在咱们的项目当中,后端koa也是一个server,所以为了简单起见,咱们可使用koa-webpack-dev-middleware来在koa当中开启webpack-dev-server。git
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 }));
在这个项目中咱们有意识的使用了flex布局,做为面向将来的一种新的布局方式,实践一下仍是颇有必要的!没有学习郭flexbox的同窗能够参考这篇来学一下:http://www.ruanyifeng.com/blog/2015/07/flex-grammar.htmlgithub
以聊天界面为例进行分析,使用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等等而言,更加的规整,使用这种思路,整个页面就像庖丁解牛通常,布局格外清晰。
项目中使用了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架构的示意图:
因为是一个即时聊天应用,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端,对应的回调函数就会被执行。经过这种方式,能够方便的编写即时通讯程序。
下面对程序中涉及的一些我认为值得注意的细节和技巧进行一下简要分析。
在程序编写过程中,我遇到一个难题,就是如何将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。
整个应用使用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文件当中实现。
在咱们的聊天应用中,若是不对用户的输入进行一些处理,就有可能致使xss漏洞。举个例子,好比说有一个用户输入了'<script>....</script>',若是不进行一些防范,输入到消息显示界面,这段文字就直接被解析成为了一段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/,会不按期更新个人原创技术文章和学习感悟,欢迎你们关注~