使用express、react、webpack打包、socket.io、mongodb、ant.design、less、es6实现聊天室

  拿到一个项目,我们应该如何去完成这个项目呢。 是直接上手? 还是先进行分析,然后再去解决呢?毫无疑问,如果直接上手解决,那么可能会因为知道目标所在,而导致出现各种问题。 所以,我们应该系统的分析这个项目,然后再去完成。 

  

第一步: 需求

  • 服务器端使用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 to bindIp: 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! 就是这么简单地解决了,所以说,每次我们需要解决一个问题时,最好是从本质、源头上解决问题。 

原文地址:https://www.cnblogs.com/zhuzhenwei918/p/7239007.html