拿到一個項目,咱們應該如何去完成這個項目呢。 是直接上手? 仍是先進行分析,而後再去解決呢?毫無疑問,若是直接上手解決,那麼可能會由於知道目標所在,而致使出現各類問題。 因此,咱們應該系統的分析這個項目,而後再去完成。 javascript
除了上面的基本需求以外,咱們還須要實現登陸、註冊的相關功能,這樣能夠保證用戶的惟一性,並在後臺作出記錄。 php
肯定技術棧是咱們須要知道爲何使用某個技術,有什麼好處,而不該該盲目的使用。css
肯定了以上技術棧以後,咱們就須要學習沒有用過的技術了。 有人提倡的是邊作項目邊學習,這種方法是沒有錯的。 可是我認爲提早學習是一種比較好的作法,即首先須要對相應技術的基本概念、api等作一個初步的瞭解,在這個基礎上作項目,咱們就能夠知道應該在遇到問題時使用那些方法來解決,這時再進入邊作項目邊學習的階段是比較理想的了。 html
好比上面的技術socket.io、redux、react-router、ant.design都是我以前沒有用過的,就須要作一個簡單的學習,大概記錄一下博客加深印象便可。 前端
實際上對於一個項目,最重要的是項目的架構實現,當咱們組織好了項目的架構並對webpack打包的部署完成以後,再去寫項目的邏輯就會簡單的多了,由於咱們已經有了總體的思路。 若是項目的總體架構考慮不周,那麼就有可能形成代碼的可讀性、可擴展性、可維護性不好,使得項目質量不高。html5
以上大概就是本項目的架構了,至於.gitignore、REDEME.md等是一個項目所必須的且不重要,再也不贅述 。 java
就是從頭開始一步一步完成這個項目,無需多說。node
作項目中不免會遇到一些問題,而且有時候還比較難解決,這時就須要咱們及時的記錄。 一來是能夠記錄問題、隨時着手解決;二來是能夠經過記錄在之後遇到問題時能夠及時的查看記錄,再也不踩相同的坑或者再去從頭尋找、思考解決思路。 react
問題1:這個須要須要使用webpack(react項目幾乎是必須使用webpack做爲打包工具的,這樣才能使用import這種語法,進行模塊的打包),同時須要node做爲後臺,那麼當組合使用的過程當中,咱們應該先開啓node服務器仍是先打包呢? 順序如何選擇?webpack
若是先開啓node服務器,而後再打包,這時就會出現錯誤 --- 由於一旦開啓了node服務器,就表示項目已經準備就緒,並開始監聽某個設定的端口,這時一旦訪問該接口,就開始提供服務了,因此必定是打包完成(爲何要打包呢? 由於本項目使用的是react,打包以後,這個頁面就是能夠展現的了),而後頁面能夠展現,當客戶端請求數據的時候(app.get('/', function () {// 將頁面發送到前端})),咱們就能夠直接將數據發送到客戶端了。 也就是說一個頁面是經過node後臺返回的,經過node,咱們看到的頁面就是node端來寫的。
問題2:這個項目時須要先後端同時來寫的,那麼後端接收到請求以後應該如何給後端返回數據呢?
後端node咱們可使用res.json() 的形式給其傳入一個對象給前端返回json數據。 遵照下面的原則:
返回的基本格式:
var response = { code: xxx, // 必須 message: xxx, // 在失敗的狀況下,屬性名稱能夠修改成 err_msg, 也能夠不是 data: xxx, // 在請求成功時能夠根據需求返回data,若是不須要data的返回,也是能夠沒有這一個字段的。 }
1、 code應當按照標準的http狀態碼來返回。
說明: 除了使用標準的狀態碼以外,咱們也能夠自定義狀態碼,原則是不要覆蓋使用標準狀態碼,而後先後端作出規則說明便可。
2、 成功時返回message,應當給予簡介的文字提示信息。失敗時應當返回err_msg來和成功時的message區分。可是這樣有一個問題,就是雖然有時是成功的,可是不是客戶端想要的,若是還返回err_msg就會出現問題。 因此統一返回message,前端可能更好處理一些。
3、 data 是咱們須要傳遞的數據,這個須要根據咱們傳遞數據的複雜性來定義其傳遞的格式,好比: 能夠是一個字符串、數值、布爾值, 也能夠是一個數組、對象等。
問題3: 當用戶點擊一個按鈕,而後發出一個請求,若是請求的結果是咱們想要的,就跳轉路由;若是不是,就不跳轉。 這種怎麼實現?
最開始個人思路是使用 react-router 的link標籤先指定路由,而後判斷的時候使用路由的鉤子函數,可是比較麻煩。
或者是使用a的preventDefault,可是這個在處理異步請求的時候會出現問題,實現思路就是錯的。
另外就是使用react-router提供的函數式編程,先從react-router中引入 browserHistory, 而後在知足條件的時候跳轉到相應的 路由便可。
問題4: 用戶登陸成功以後,應該如何進行管理用戶 ?
本項目不管登陸仍是註冊,一旦成功,都會導向主頁面,那麼當前用戶的信息如何持久保存呢?
咱們從兩個方面來考慮:
客戶端保存數據有兩種思路:
第一種: 保存在本地的localStorage中,不一樣的用戶都會持久保存,對於正常的業務是沒有問題的。 可是在開發過程當中,服務器是在本地開啓的, 多人聊天只有開發者一我的來測試,因此使用這種方法的問題就在於在同一個瀏覽器中打開多個標籤頁就會出現相互覆蓋的問題。 固然,咱們還能夠採用使用多個瀏覽器的方式來避免這一問題,這樣localStorage是不會相互覆蓋的。
第二種: 使用redux來管理這個用戶。 即每當我登陸成功或者註冊成功以後,將當前用戶保存在redux的倉庫裏,這樣,後續我就能夠從這個倉庫裏隨時取到這個 user 了。 而且對於在同一個瀏覽器中打開多個標籤頁進行測試也是沒有問題的。
綜合上述兩種思路,仍是選擇使用第二種會比較好一些。
當客戶端登陸成功以後就會進入主頁面,而後開始創建socket鏈接,在node端的socket是針對一個用戶就有一個socket,那麼咱們就能夠將這個socket.name添加到其中一個房間中。 固然,用戶還能夠添加到其餘房間中,若是須要建立房間,只須要添加個房間數組便可。 而且在廣播時,應當對用戶所在的房間進行廣播。
而且對於用戶發送的每一條記錄,咱們都須要根據不一樣的房間建立數據庫進行存儲,這樣,咱們就能夠在用戶下次登陸進入這個房間的時候將歷史消息推送過來。
至於歷史消息的推送,咱們就不能採用socket的方式了,由於採用socket解決的是及時性的問題。因此最好使用http進行推送。 可是呢? 在進入房間的時候,咱們應當如何控制最新消息和歷史消息的順序呢?
問題五、 對於用es6建立的組件中的自定義函數的this的指向,爲何每次都要在construtor中來綁定this呢?
以下:
class LogX extends React.Component { constructor(props) { super(props); this.state = { userName : "", password : "", passwordAgain : "" } this.handleChangeUser = this.handleChangeUser.bind(this); this.handleChangePass = this.handleChangePass.bind(this); this.handleChangePassAgain = this.handleChangePassAgain.bind(this); this.handleLog = this.handleLog.bind(this); } // 經過對 onchange 事件的監控,咱們可使用react獨特的方式來獲取到value值。 handleChangeUser (event) { this.setState({userName: event.target.value}); } handleChangePass (event) { this.setState({password: event.target.value}); } handleChangePassAgain (event) { this.setState({passwordAgain: event.target.value}); } // ... }
能夠看到,對於咱們自定義的函數,必須在constructor中綁定this。這是由於,經過console.log咱們能夠發現若是沒有綁定在嚴格環境下 this 指向的是 null,在非嚴格環境下指向的就是window,而constructor中咱們能夠綁定this,這樣就能夠在render的組件中使用 this.xxx 來調用這些自定義的函數了 。
問題6: 在客戶端這邊須要建立房間時,客戶端和服務器端應該如何處理?
首先點擊建立房間時,彈出一個框,用於輸入房間名稱,接着,咱們就面臨將數據放在哪裏的問題 ?
方法一: 只放在redux中的store裏。
這個方法固然是能夠的,全部的房間均可以本地的store,可是問題是,其餘的用戶沒法及時看到你建立的房間,別人怎麼才能加進來呢? 因此不能直接放在store裏。
結果: 不可行。
方法二: 在用戶建立了房間以後,將數據發送到服務器端, 而後在服務器端新建一個集合,專門用於存儲房間的名稱,因此這樣保證房間名是不能重複的。 而後服務器端再經過websocket將這個新的房間名稱廣播到各個用戶,這時,用戶就須要把從服務器端接收到的房間名稱存儲(push)在本地store中,由於在鏈接服務器時服務器端就應該已經將信息推送到瀏覽器端了,而後顯示在頁面上,每當用戶切換房間時,服務器端就經過websocket將全部通訊的信息發送到客戶端便可 。
固然,這也就要求咱們每次再連接服務器時,首先服務器須要將房間數據庫中的全部房間名稱所有發送到本地,而後存儲在store中便可。
結果:可行。
須要注意的問題: 當咱們但願建立一個新房間時,輸入房間名稱以後,咱們應當先經過http請求向後臺確認這個名字是否重複,若是沒有重複咱們才能夠建立,若是重複了,咱們須要提示用戶。 即重要的點在於: 正確區分何時使用http請求,何時使用websocket請求。
問題7:到插入數據的一步中,若是我只是在發生錯誤的時候才關閉數據庫,而不是不管是否有錯在第一步就關閉數據庫,node服務器就會發生崩潰,爲何? 以下所示:
RoomName.saveOne = function (name, callback) { mongodb.open(function (err, db) { if (err) { return callback(err); } db.collection('allRooms', function (err, collection) { if (err) { mongodb.close(); return callback(err); } collection.insert({ name: name }, function (err) { // XXX FIXME if (err) { mongodb.close(); return callback(err); } callback(null); }); }); }); }
可是,若是黑體部分爲下面的形式,node服務器就不會崩潰:
collection.insert({ name: name }, function (err) { // XXX FIXME mongodb.close(); if (err) { return callback(err); } callback(null); });
即若是說最後一步必須關閉掉數據庫,那麼就不會出現報錯的狀況。
問題8: 和問題7相似,就是僅僅打開數據庫的時候,就出現報錯,後臺崩潰,錯誤以下:
在stackoverflow上能夠看到相似問題的文章: https://stackoverflow.com/questions/40299824/mongoerror-server-instance-in-invalid-state-undefined-after-upgrading-mongoose
Here is the solution of my case. This error occurs when Mongoose connection is started and you try to access database before the connection is finished.
In my case, my MongoDB is running in a Docker Container which exposes the 27017 port. To be able to expose the port outside the Container, the mongod process inside Container must listen to 0.0.0.0 and not only 127.0.0.1 (which is the default). So, the connect instruction hangs and program try to access collections before it ends. To fix it, simply change the /etc/mongod.conf file and change
bindIp: 127.0.0.1
tobindIp: 0.0.0.0
I guess the error should be more comprehensive for human being... Something like "connection started but not finished" will be better for our understanding.
大概意思就是在尚未連接到數據庫的時候,就已經開始想要打開數據庫了,即這個差錯的事件致使報錯,即找不到數據庫,因此咱們解決的辦法能夠是延長一段事件再打開數據庫。
問題九、多個房間的通訊數據應該是如何整理的?
前端發送給後端的信息中必須還須要包含用戶所在的聊天室,這樣後端才能夠根據不的信息存放在不一樣的聊天室中。 而後後端向用戶羣發消息時,用戶經過判斷此消息是不是當前聊天室的,若是不是,就不要,若是是,就留下進行展現,而且咱們認爲前端的redux倉庫中只能保存一份聊天室的數據,每當用戶切換聊天室時,後端就根據聊天室的狀況從數據庫中取出向前端發送數據。
而且在咱們發送信息時,已經知道須要保存room信息,可是在存儲到mongodb數據庫的時候,是不須要有room的kv的,這個是沒有必要的。
問題十、 在接收服務器端發送來的數據的時候,須要比對數據中房間和本地的當前房間是不是相等的t,若是相等,就把數據添加到本地的state中;若是不相等,就不接收。下面的前二者都會出現問題?
失敗一、
this.socket.on('newText', function (info) { console.log(info) // 若是服務器發送過來的房間和當前房間一致,就添加; 不然,不添加。 const {curRoomName} = this.props; var doc = document; if (info[3] == curRoomName) { this.props.addNewList(info); doc.querySelector('.lists-wrap').scrollTop = doc.querySelector('.lists-wrap').scrollHeight - doc.querySelector('.lists-wrap').clientHeight } });
這段代碼是在 componentDidMount 鉤子函數中的, curRoomName 是從redux中的state中獲取的,可是這段代碼的問題是: 這裏的 curRoomName 始終是不變的,由於 componentDidMount 僅僅在第一次渲染以後調用,後面都不會調用、從新渲染,因此 curRoomName 也就始終拿不到最新的數據。
失敗2、
那麼若是把這段代碼添加到 componentDidUpdate 中去呢? 結果發現還真是有效,可是獲得的數據是不少份,由於 componentDidUpdate 只要 state 發生了改變,這個鉤子函數就會從新調用, 因此這裏的 this.socket.on 可能被註冊了不少次,致使的結果就是數據有多分。
成功:
在 compoentDidMount 中代碼以下:
this.socket.on('newText', function (info) { console.log(info) // 若是服務器發送過來的房間和當前房間一致,就添加; 不然,不添加。 that.receiveNewText(info); });
而後咱們在組件中定義了 receiveNewText 函數,以下:
receiveNewText(info) { const {curRoomName} = this.props; var doc = document; if (info[3] == curRoomName) { this.props.addNewList(info); doc.querySelector('.lists-wrap').scrollTop = doc.querySelector('.lists-wrap').scrollHeight - doc.querySelector('.lists-wrap').clientHeight } }
問題十一、 咱們在使用node做爲服務器時,怎麼樣才能在修改的時候保證最大的效率?
對於webpack打包(前端代碼),咱們可使用 webpack -w 的方式,這樣,只要檢測到文件變化,就會自動打包。
對於node端代碼的修改,咱們在啓動的時候,如 node ./build/dev-server.js 的時候,若是隻是這樣,那麼修改一次node端的代碼,咱們就須要重啓一次,這樣很麻煩。 因此,咱們能夠先在全局安裝一個 supervisor 包。
npm install -g supervisor
而後拿到這個包以後,咱們在啓動的時候能夠是下面這樣的命令:
supervisor node ./build/dev-server.js
這樣,每當咱們修改服務器端的代碼的時候, supervisor都會監測到變化,而後開啓一個進程來從新開啓這個服務器,這樣,就不用咱們每次手動的去處理了。
問題十二、 每次咱們都
問題十二、 在使用socket.io的時候,咱們能夠發現,在官方教程中,通常的設置以下。
服務器端:
// 建立一個express實例 var app = express() // 基於express實例建立http服務器 var server = require('http').Server(app); // 建立websocket服務器,以監聽http服務器 var io = require('socket.io').listen(server);
即首先建立一個express實例,而後建立一個http server,接着使用 socket 來監聽這個 http 服務器。
客戶端:
<body> <div id="app"></div> <script type="text/javascript" src="./js/bundle.js"></script> <script src='/socket.io/socket.io.js'></script> </body>
客戶端直接引入了 /socket.io/socket.io.js,可是在 socket.io 的node_modules中是沒有這個文件的?而且這個也不是靜態文件的內容。 那麼這個文件是如何引入的呢?
因而,通過測試,這是咱們在使用服務器端開啓 socket 服務器的時候,默認監聽了這個api,一旦請求,就會發送這個js文件。
普通驗證:
在啓動node服務器的時候,不連接 socket ,而後咱們再次打開文件, 能夠發現,並無獲取到這個js文件。因此,src 確實是向socket服務器發出了一個get請求。
不管如何,源碼是不會騙人的,咱們能夠在源碼中搜尋答案進行驗證:
README.md
在 socket.io 的源碼中(socket.io-client/README.md)裏,咱們能夠看到下面的這樣一段說明:
## How to use A standalone build of `socket.io-client` is exposed automatically by the socket.io server as `/socket.io/socket.io.js`. Alternatively you can serve the file `socket.io.js` found in the `dist` folder. ```html <script src="/socket.io/socket.io.js"></script> <script> var socket = io('http://localhost'); socket.on('connect', function(){}); socket.on('event', function(data){}); socket.on('disconnect', function(){}); </script> ```
即 socket.io-client 已經自動被 socket.io 服務器暴露出來了。 另外,能夠供選擇的,你能夠在dist文件夾下找到 socket.io.js 文件,引用方式。
可是具體是怎麼暴露出來的,它並無說,這就須要咱們本身去探索了。
咱們首先進入主文件,這個文件的主要做用就是建立一個Server構造函數,而後在這個函數原型上添加了不少方法,接着導出這個函數。
function Server(srv, opts){ if (!(this instanceof Server)) return new Server(srv, opts); if ('object' == typeof srv && srv instanceof Object && !srv.listen) { opts = srv; srv = null; } opts = opts || {}; this.nsps = {}; this.path(opts.path || '/socket.io'); this.serveClient(false !== opts.serveClient); this.parser = opts.parser || parser; this.encoder = new this.parser.Encoder(); this.adapter(opts.adapter || Adapter); this.origins(opts.origins || '*:*'); this.sockets = this.of('/'); if (srv) this.attach(srv, opts); }
一個Server實例一旦被建立,就會自動初始化下面的一些屬性,在這些屬性中,我悶看到了 this.serveClient(false !== opts.serveClient) 這個方法的初始化,通常,在服務器端建立實例時咱們是沒有添加serveClient配置的,這樣 opts.serveClient 的值就是undefined,因此,就會調用 this.serveClient(true); 接下來咱們看看 this.serveClient() 這個函數式如何執行的。
這個函數以下,在 client code 被提供的時候會進行以下調用,其中 v 是一個布爾值。
/** * Sets/gets whether client code is being served. * * @param {Boolean} v whether to serve client code * @return {Server|Boolean} self when setting or value when getting * @api public */ Server.prototype.serveClient = function(v){ if (!arguments.length) return this._serveClient; this._serveClient = v; var resolvePath = function(file){ var filepath = path.resolve(__dirname, './../../', file); if (exists(filepath)) { return filepath; } return require.resolve(file); }; if (v && !clientSource) { clientSource = read(resolvePath( 'socket.io-client/dist/socket.io.js'), 'utf-8'); try { clientSourceMap = read(resolvePath( 'socket.io-client/dist/socket.io.js.map'), 'utf-8'); } catch(err) { debug('could not load sourcemap file'); } } return this; };
若是沒有參數,那麼就返回 this._serveClient 這個值,他是 undefined。 再也不執行下面的代碼。
若是傳入了參數,就設置 _serveClient 爲 v,而且定義一個處理路徑的函數,接着判斷 v && !clientSource 的值,其中clientSource在本文件開頭定義爲 undefined,顯然,clientSource意思就是提供給客戶端的代碼。 那麼 v&&!clientSource 的值就是true,繼續執行下面的函數,這裏很關鍵,從socket.io-client/dist/socket.io.js中讀取賦值給clientSource, 這個文件就是咱們在前端請求的文件,可是具體是怎麼提供的呢? 咱們繼續向下看。而後又嘗試讀取map, 若是有的話 ,就添加到 clientSourceMap中。
因此,咱們只須要知道 clientSource 是如何被提供出去的, 這時,咱們能夠在文件中繼續搜索 clientSource 這個關鍵字,看他還出如今了哪些地方,不出意料,仍是在 index.js 文件中,咱們找到了 Server.prototype.serve 函數中使用了 clientSource。
Server.prototype.serve = function(req, res){ // Per the standard, ETags must be quoted: // https://tools.ietf.org/html/rfc7232#section-2.3 var expectedEtag = '"' + clientVersion + '"'; var etag = req.headers['if-none-match']; if (etag) { if (expectedEtag == etag) { debug('serve client 304'); res.writeHead(304); res.end(); return; } } debug('serve client source'); res.setHeader('Content-Type', 'application/javascript'); res.setHeader('ETag', expectedEtag); res.writeHead(200); res.end(clientSource); };
顯然,這裏能夠看到,首先獲取了 expectedEtag ,而後又從請求中獲取了 etag ,若是etag存在,即客戶端但願使用緩存,就會比較 expectedEtag 值和 eTage 值是否相等,若是相等, 就返回304,讓用戶使用緩存,不然,就會提供用戶新的eTag,而後狀態碼200, 接着把 clientSource 返回 。 可是這裏卻沒有對req進行判斷,只是直接返回了 clientSource ,因此,必定是在某個地方對 serve 函數進行了調用, 在調用前判斷用戶發出的get請求(script 中的src必定會觸發get請求)是否知足條件,若是知足條件,就執行 serve 函數。
既然,serve是在prototype上的,調用的時候必定是 this.serve() 調用,因此咱們能夠嘗試搜索 this.serve ,可是沒有搜索到,咱們能夠繼續使用 that.serve 和 self.serve來進行搜索, 果真,使用 self.serve搜索時就搜索到了。
這個函數的主要內容以下:
Server.prototype.attachServe = function(srv){ debug('attaching client serving req handler'); var url = this._path + '/socket.io.js'; var urlMap = this._path + '/socket.io.js.map'; var evs = srv.listeners('request').slice(0); var self = this; srv.removeAllListeners('request'); srv.on('request', function(req, res) { if (0 === req.url.indexOf(urlMap)) { self.serveMap(req, res); } else if (0 === req.url.indexOf(url)) { self.serve(req, res); } else { for (var i = 0; i < evs.length; i++) { evs[i].call(srv, req, res); } } }); };
能夠看到,這裏的url就是對咱們使用script進行get請求時的url,而後urlMap相似,接着開始對全部的request請求進行監聽, 當有請求來到時,判斷 是否有 urlMap,若是有,就調用 serveMap 給前端; 接着判斷是否有相同的url,若是有,就調用 self.serve(req, res); 這樣就達到咱們的目的了。
那麼 attchServe這個函數什麼時候被調用呢,咱們直接搜索 attchServe便可,找到了 initEngine 函數。
/** * Initialize engine * * @param {Object} options passed to engine.io * @api private */ Server.prototype.initEngine = function(srv, opts){ // initialize engine debug('creating engine.io instance with opts %j', opts); this.eio = engine.attach(srv, opts); // attach static file serving if (this._serveClient) this.attachServe(srv); // Export http server this.httpServer = srv; // bind to engine events this.bind(this.eio); };
這個函數中就是當 this._serveClient 爲true時(以前的 serverClient 不傳遞參數就是true了),就開始調用這個函數。 那麼 initEngine又是何時執行的呢? 咱們繼續在文件中搜索 initEngine, 找到了 Server.prototype.listen和Server.prototype.attach函數。
/** * Attaches socket.io to a server or port. * * @param {http.Server|Number} server or port * @param {Object} options passed to engine.io * @return {Server} self * @api public */ Server.prototype.listen = Server.prototype.attach = function(srv, opts){ if ('function' == typeof srv) { var msg = 'You are trying to attach socket.io to an express ' + 'request handler function. Please pass a http.Server instance.'; throw new Error(msg); } // handle a port as a string if (Number(srv) == srv) { srv = Number(srv); } if ('number' == typeof srv) { debug('creating http server and binding to %d', srv); var port = srv; srv = http.Server(function(req, res){ res.writeHead(404); res.end(); }); srv.listen(port); } // set engine.io path to `/socket.io` opts = opts || {}; opts.path = opts.path || this.path(); // set origins verification opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this); if (this.sockets.fns.length > 0) { this.initEngine(srv, opts); return this; } var self = this; var connectPacket = { type: parser.CONNECT, nsp: '/' }; this.encoder.encode(connectPacket, function (encodedPacket){ // the CONNECT packet will be merged with Engine.IO handshake, // to reduce the number of round trips opts.initialPacket = encodedPacket; self.initEngine(srv, opts); }); return this; };
能夠看到只要把一個socket.io來監聽某個端口時,就會執行這個函數了。當知足 this.sockets.fns.length > 0 ,就會執行 initEngine 函數,這樣,就會繼續執行上面的一系列步驟了。
OK! 就是這麼簡單地解決了,因此說,每次咱們須要解決一個問題時,最好是從本質、源頭上解決問題。