拿到一个项目,咱们应该如何去完成这个项目呢。 是直接上手? 仍是先进行分析,而后再去解决呢?毫无疑问,若是直接上手解决,那么可能会由于知道目标所在,而致使出现各类问题。 因此,咱们应该系统的分析这个项目,而后再去完成。 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! 就是这么简单地解决了,因此说,每次咱们须要解决一个问题时,最好是从本质、源头上解决问题。