最近練手開發了一個項目,是一個聊天室應用。項目雖不大,可是使用到了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/,會不按期更新個人原創技術文章和學習感悟,歡迎你們關注~