拿到一個項目,我們應該如何去完成這個項目呢。 是直接上手? 還是先進行分析,然后再去解決呢?毫無疑問,如果直接上手解決,那么可能會因為知道目標所在,而導致出現各種問題。 所以,我們應該系統的分析這個項目,然后再去完成。
第一步: 需求
- 服務器端使用nodejs
- 可以加入現有的房間
- 可以創建新的房間
- 用戶可以文字聊天
- 聊天記錄永久保存
除了上面的基本需求之外,我們還需要實現登錄、注冊的相關功能,這樣可以保證用戶的唯一性,並在后台做出記錄。
第二步:確定技術棧
確定技術棧是我們需要知道為什么使用某個技術,有什么好處,而不應該盲目的使用。
- express --- 首先,作為前端使用node就可以取代后端java、php開發的工作,對於這個項目是必須的。作為node的框架,express可以幫助我們減少不必要的代碼,從而高效完成工作。
- react、react-router、redux --- 作為非常流行的前端框架,組件化的設計思想是本項目的最大優勢,可以進行嘗試使用。因為本項目需要使用登錄、注冊,所以要做成web單頁面應用,因為需要使用到react-router。另外,對於數據的管理較為復雜,需要使用到redux, 對於redux我們可以查看這篇文章。
- webpack打包 --- webpack是當前最流行的打包工具,通過webpack,我們可以實現前端工程化,對代碼的管理以及后期的維護都有很大的幫助,但是可能上手不太容易,需要花費時間進行探索。
- socket.io --- socket.io是對websock(實現全雙工通信的應用層協議)的封裝,對於實時的聊天很有幫助。 因為聊天需要某個人發送消息給服務器端, 但是其他用戶怎么快速得到你的消息呢? 這就需要服務器端及時將這個消息推送到其他的用戶了,但是其他用戶並沒有向服務器端發出請求,所以原來采用的就是輪詢的方式,通過這種方式可以完成功能,但是會增加客戶端和服務器端的負擔。 這篇文章也介紹了一些使用場景。(注意: 有時候,也許我們在請求中看不到websocket協議相關,而是http協議,這也是正常的,因為有可能瀏覽器后者服務器不支持,就要使用其他方式來實現。)
- mongodb --- 因為項目需求中提到刷新頁面之后,還需要展示加入過的房間的歷史聊天記錄,通常在前端可能是可以通過localStorage來實現的,但是使用localStorage是有問題的,對於其他人的推送消息,我們都需要調用localStorage,並且如果你一旦更換瀏覽器,那么數據就沒有辦法保存了。 但是如果我們使用mongodb作為node來操作的數據庫,那么我們就可以在用戶進入某個房間的時候,及時將存儲在數據庫中的數據(所以,每次用戶發送的數據,都要根據相應的房間號存儲到mongodb數據庫中)推送給用戶。
- less --- 對於html5,在某些pc瀏覽器上可能支持的不好,所以目前並沒有廣泛使用,但是對於less,它使用嵌套語法、變量、minxin等使得css的書寫更加清晰、整潔,並且它最終是可以編譯為css的,所以鼓勵使用less。
- es6 --- 同less一樣,我們使用es6可以使得語法更簡潔,效率更高效,只需要使用babel進行轉譯即可,所以推薦所有的項目都使用es6甚至是es7的語法來實現。
第三步: 技術學習
確定了以上技術棧之后,我們就需要學習沒有用過的技術了。 有人提倡的是邊做項目邊學習,這種方法是沒有錯的。 但是我認為提前學習是一種比較好的做法,即首先需要對相應技術的基本概念、api等做一個初步的了解,在這個基礎上做項目,我們就可以知道應該在遇到問題時使用那些方法來解決,這時再進入邊做項目邊學習的階段是比較理想的了。
比如上面的技術socket.io、redux、react-router、ant.design都是我之前沒有用過的,就需要做一個簡單的學習,大概記錄一下博客加深印象即可。
第四步: 項目架構
實際上對於一個項目,最重要的是項目的架構實現,當我們組織好了項目的架構並對webpack打包的部署完成之后,再去寫項目的邏輯就會簡單的多了,因為我們已經有了整體的思路。 如果項目的整體架構考慮不周,那么就有可能造成代碼的可讀性、可擴展性、可維護性很差,使得項目質量不高。
- build文件夾 - node服務器相關文件。
- models文件夾 - 后端采用MVC架構,此文件夾下存放的是操作數據庫相關文件。
- router文件夾 - 即controller部分。
- src文件夾 - 這部分文件是react項目相關文件,包括components、pages、redux等。
- www文件夾 - 即項目的靜態文件。
- package.json - 該文件記錄了整個項目的基本信息如入口、名稱、倉庫、依賴等等。
- settings.js - 一些數據庫設置。
- ...
以上大概就是本項目的架構了,至於.gitignore、REDEME.md等是一個項目所必須的且不重要,不再贅述 。
第五步: 開始寫代碼
就是從頭開始一步一步完成這個項目,無需多說。
第六步: 遇到的難點以及解決思路、方案
做項目中難免會遇到一些問題,並且有時候還比較難解決,這時就需要我們及時的記錄。 一來是可以記錄問題、隨時着手解決;二來是可以通過記錄在以后遇到問題時可以及時的查看記錄,不再踩相同的坑或者再去從頭尋找、思考解決思路。
問題1:這個需要需要使用webpack(react項目幾乎是必須使用webpack作為打包工具的,這樣才能使用import這種語法,進行模塊的打包),同時需要node作為后台,那么當組合使用的過程中,我們應該先開啟node服務器還是先打包呢? 順序如何選擇?
如果先開啟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的返回,也是可以沒有這一個字段的。 }
一、 code應當按照標准的http狀態碼來返回。
- 成功時,我們可以返回狀態碼200來表示對數據請求成功。
- 失敗時,需要針對不同的情況來對客戶端進行反饋。
- 如果是請求的參數有誤,比如一般的參數錯誤,服務器端無法理解,或者是注冊時兩次的密碼不一致等,我們可以返回狀態碼400 Bad Request表示無法理解請求參數。
- 如果是打開數據庫錯誤,就說明服務器端出現的錯誤,可以返回 500 錯誤,即服務器端的錯誤。
說明: 除了使用標准的狀態碼之外,我們也可以自定義狀態碼,原則是不要覆蓋使用標准狀態碼,然后前后端做出規則說明即可。
二、 成功時返回message,應當給予簡介的文字提示信息。失敗時應當返回err_msg來和成功時的message區分。但是這樣有一個問題,就是雖然有時是成功的,但是不是客戶端想要的,如果還返回err_msg就會出現問題。 所以統一返回message,前端可能更好處理一些。
三、 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進行推送。 但是呢? 在進入房間的時候,我們應當如何控制最新消息和歷史消息的順序呢?
問題5、 對於用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.
大概意思就是在還沒有鏈接到數據庫的時候,就已經開始想要打開數據庫了,即這個差錯的事件導致報錯,即找不到數據庫,所以我們解決的辦法可以是延長一段事件再打開數據庫。
問題9、多個房間的通信數據應該是如何整理的?
前端發送給后端的信息中必須還需要包含用戶所在的聊天室,這樣后端才可以根據不的信息存放在不同的聊天室中。 然后后端向用戶群發消息時,用戶通過判斷此消息是否是當前聊天室的,如果不是,就不要,如果是,就留下進行展示,並且我們認為前端的redux倉庫中只能保存一份聊天室的數據,每當用戶切換聊天室時,后端就根據聊天室的情況從數據庫中取出向前端發送數據。
並且在我們發送信息時,已經知道需要保存room信息,但是在存儲到mongodb數據庫的時候,是不需要有room的kv的,這個是沒有必要的。
問題10、 在接收服務器端發送來的數據的時候,需要比對數據中房間和本地的當前房間是否是相等的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 也就始終拿不到最新的數據。
失敗二、
那么如果把這段代碼添加到 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 } }
問題11、 我們在使用node作為服務器時,怎么樣才能在修改的時候保證最大的效率?
對於webpack打包(前端代碼),我們可以使用 webpack -w 的方式,這樣,只要檢測到文件變化,就會自動打包。
對於node端代碼的修改,我們在啟動的時候,如 node ./build/dev-server.js 的時候,如果只是這樣,那么修改一次node端的代碼,我們就需要重啟一次,這樣很麻煩。 所以,我們可以先在全局安裝一個 supervisor 包。
npm install -g supervisor
然后拿到這個包之后,我們在啟動的時候可以是下面這樣的命令:
supervisor node ./build/dev-server.js
這樣,每當我們修改服務器端的代碼的時候, supervisor都會監測到變化,然后開啟一個進程來重新開啟這個服務器,這樣,就不用我們每次手動的去處理了。
問題12、 每次我們都
問題12、 在使用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 文件,引用方式。
但是具體是怎么暴露出來的,它並沒有說,這就需要我們自己去探索了。
socket.io/lib/index.js主文件 Server構造函數
我們首先進入主文件,這個文件的主要作用就是創建一個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() 這個函數式如何執行的。
socket.io/lib/index.js主文件 Server.prototype.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。
socket.io/lib/index.js主文件 Server.prototype.serveClient() 函數
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搜索時就搜索到了。
socket.io/lib/index.js主文件 Server.prototype.attachServe
這個函數的主要內容如下:
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 函數。
socket.io/lib/index.js主文件 Server.prototype.attachServe
/** * 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函數。
socket.io/lib/index.js主文件 Server.prototype.attachServe
/** * 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! 就是這么簡單地解決了,所以說,每次我們需要解決一個問題時,最好是從本質、源頭上解決問題。