nodejs基础知识查缺补漏
1. 单线程、异步I/O、对比php
nodejs是单线程的,但是是异步I/O,对于高并发时,它也能够快速的处理请求,100万个请求也可以承担,但是缺点是非常的耗内存,但是我们可以加大内存, 所以能用钱解决的事就不是事。 而PHP语言是使用Apache服务器的,它的方法和node是完全不一样的,虽然php也是单线程的,但是apache是支持多线程的,它的方法是派出150个线程放在线程池中,然后对于需要的程序来从线程池中取得线程,用完了之后再放回去。 显然,php做的后台在速度上是不如node的(一般情况下是这样),因为node是支持高并发的。
不仅如此, nodejs的相较于php开发效率非常高。 这并不是说它代码写的少,而是他在优化方面做得少一些就可以达到很好的效率,而php就要做大量的优化。
nodejs应用场景广泛, 但是目前在xp系统的某些应用还不能支持。
缺点是nodejs是非常新的, 所以可能在人才方面不够充足,很多公司不会轻易使用node, 而php有很多成熟的中间件,大量的从业人员,所以就目前来说,php更加稳定一些。且node的IDE并吧完善。
2. 框架
目前用的比较多的就是express,更稳定一些。
而koa也是一个node框架,但是这个框架比较新,还不够稳定,并且它是完全基于es6的。
Hapi框架比较适合大型网站,入门较难。
sails框架也不错,是基于express的。
3. 我们使用下面的代码创建服务器时,会发现每次访问了一次,却console.log两次,如下所示:
var http = require("http"); http.createServer(function (req, res) { res.writeHead(200, {"Content-Type": "text/html"}); console.log("访问"); res.write("Hello world!"); res.end(); }).listen(8081); console.log("Server running at http:127.0.0.1:8081");
注意1: 其中的end()表示response结束,如果不添加,就会发现浏览器页面上会一直转圈,这实际上是因为没有end所以服务器一直在等待接收资源,正如我们打开一个资源较多的网站,如果资源没有加载完成,那么就一直会转圈。。。另外,在res.end()中可以输出一些内容到页面上,也可以不输出。
注意2: 我们可以console.log("访问")应该在dos只有一次,但是每当我们刷新的时候就会发现同时出现了两次,这实际上可以认为是nodejs的bug,我们需要通过添加一句话来解决这个问题(在express框架中的底层中已经解决了这个问题,但是我们使用原生nodejs的时候还是需要注意的)。
var http = require("http"); http.createServer(function (req, res) { res.writeHead(200, {"Content-Type": "text/html"}); if (req.url !== "/favicon.ico") { // 清除第二次访问 console.log("访问"); res.write("Hello world!");
res.end(); } }).listen(8081); console.log("Server running at http:127.0.0.1:8081");
如此,就可以解决问题了。(注意:一定是req.url !== "/favicon.ico")
4. 模块调用(类的继承)
下面我们解决这样一个问题,在当前目录建立一个server.js作为node后台,然后在当前目录下再创建一个modules目录,在modules目录下面创建三个模块: User、Teacher、Student。其中后两者继承前者的。前者包含最基本的id、name和age,以及一个enter方法,表示进入,在后台中打印出来。 Teacher在继承了User的基础上,又有自己的方法teach。 同样,Student在继承了User的基础上,也有自己的方法teach。
由于ji是弱类型的,所以继承相对宽松一些,不像C++一样,有公有的、私有的、受保护的等等。 整体的架构如下:
其中User.js内容如下:
function User(id, name, age) { this.id = id; this.name = name; this.age = age; this.enter = function () { console.log(this.name + "进入系统"); } } module.exports = User;
即这是一个构造函数, 然后因为就一个,所以直接使用module.exports = User;即可。
其中Teacher.js代码如下所示:
var User = require("./User.js"); function Teacher(id, name, age) { User.apply(this, [id, name, age]); this.teach = function (res) { res.write(this.name + "正在上课"); } } module.exports = Teacher;
可以看到,这里我们使用借用构造函数的方法,这与原型继承是不同的,另外,我给Teacher类又添加了一个自己的方法teach,并需要调用参数res。
类似的,Student.js代码如下所示:
var User = require("./User"); function Student(id, name, age) { User.apply(this, [id, name, age]); this.learn = function (res) { res.write(this.name + "正在学习"); } } module.exports = Student;
server.js如下所示:
var http = require("http"); var User = require("./modules/User"); var Teacher = require("./modules/Teacher"); var Student = require("./modules/Student"); http.createServer(function (req, res) { res.writeHead(200, {"Content-Type": "text/html; charset=utf8"}); if (req.url !== "/favicon.ico") { var student = new Student(1, "John Zhu", 21); console.log(student.enter()); student.learn(res); res.end(); } }).listen(8000); console.log("Server is running at http://127.0.0.1:8000");
即将这几个模块都引入了进来,然后通过初始化构造函数创建实例, 最后调用了一些方法。
这个例子就展示了类与继承、模块引入等方法。
5. nodejs路由
这是一个重要的概念,之前一直都没有很好的理解,所以这里着重整理。
其实很简单,就是输入 http://127.0.0.1:8000/login, 那么服务端就返回与login相关的respond。 方法就是通过在server.js中拿到url,在拿到login,然后找到对应的js文件,拿到对应的方法,再将相应的文件显示到页面上即可。
话不多说,举例如下:
router.js如下:
module.exports = { login: function (req, res) { res.write("进入login页面"); }, register: function (req, res) { res.write("进入register页面"); } };
server.js如下:
var http = require("http"); var url = require("url"); var router = require("./router"); http.createServer(function (req, res) { res.writeHead(200, {"Content-Type": "text/html; charset=utf8"}); if (req.url !== "/favicon.ico") { var path = url.parse(req.url).pathname; path = path.replace(/\//, ""); router[path](req, res); res.end(); } }).listen(8888); console.log("Server is running at http://127.0.0.1:8888");
即引入路由模块,然后使用url模块中的parser方法解析url得到用户输入的path,并使用正则替换不必要的/,最后再利用js中的[]来调用方法,达到路由的目的。
6. nodejs文件读取
nodejs中文件读取有两种,一种是同步,一种是异步,同步是指读完之后再做下一件事情,异步是不等读完就做下一件事情,类似于又开了一个线程。
同步执行非常简单,就是执行完一个执行另一个,下面主要看同步会出现的问题,如下:
server.js如下:
var http = require("http"); var url = require("url"); var optfile = require("./modules/optfile"); http.createServer(function (req, res) { res.writeHead(200, {"Content": "utf8; charset=utf8"}); if (req.url !== "/favicon.ico") { function recall(data) { res.write(data); res.end(); } optfile.readfile("./view/login.html", recall); console.log("主程序执行完毕"); } }).listen(8888); console.log("server is running at 127.0.0.1:8888");
然后optfile.js如下:
var fs = require("fs"); module.exports = { readfile: function (path, recall) { fs.readFile(path, function (err, data) { if (err) { console.log(err); } else { recall(data); } }); console.log("异步方法执行完毕!"); }, readfileSync: function (path) { var data = fs.readFileSync(path,"utf-8"); console.log(data.toString()); console.log("同步方法执行完毕"); } };
login.html如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>login</title> </head> <body> <p>login.html</p> </body> </html>
最终运行输出如下:
为什么"server is running at 127.0.0.1:8888"总是最先打出来?这是因为在node中所有的api都是异步的,所以http.createServer也是异步的,于是不等执行完, 就跳过去执行console.log("server is running at 127.0.0.1:8888");了.
另外,为什么异步方法执行完毕在主程序执行完毕之前呢?
因为虽然readFile是异步的,但是我们自己创建的readfileSync确是同步的,只有在console.log("异步。。")执行完毕,这个同步的才执行完毕,所以console.log('同步。。')才会之后输出。
问题: 为什么一定要使用闭包呢? 如果我不传入recall,而只是将res.write()和res.end()下载optfile.js中else下面可行吗?
不可行!!!
因为进入readfile(这是同步的)之后,readFile是一个异步的,所以立即执行了console.log("异步。。")。然后栈退回到了http.createServer,这是这个函数也立即执行完毕,所以res作为局部变量就立即被释放了,而这是处在另一个线程上读完文件后,可能再用res,就已经被销毁了,而不能找到。
但是使用了闭包之后就不一样了,使用了闭包,就会对res有一个引用,即使是在函数执行完之后,也不会释放res。
可我直接将res.write()和res.end()放在optfile.js中else下面时,也是在一个函数里啊,然后保存了对外层函数中的引用,这难道就不是闭包吗???
这里我们可以先粗暴的理解,闭包必须是同步的,而不能是异步的!
7. 读取并显示图片
var http = require("http"); var fs = require("fs"); http.createServer(function (req, res) { res.writeHead(200, {"Content-Type": "image/jpeg"}); if (req.url !== "/favicon.ico") { fs.readFile("./pig.jpg", "binary", function (err, data) { if (err) { console.log(err); } else { res.write(data, "binary"); res.end(); } }); } }).listen(8888); console.log("Server is running at http://127.0.0.1:8888");
这里区别并不大,之前使用的是text/plain,而这里使用的是image/jpeg,虽然,最终我们读取的是jpg图片,但是即使我们读取png图片,最后也是可以的。
注意,在readFile中的第二个参数中,是接受编码的类型,普通的我们也可以写成"utf-8", 当是读取图片时 ,就要写成"binary"。
经过尝试,我们可以发现Content-Type的值也可以是image/png或者是image/jpg都是可以的。但是如果换成其他的就会出问题了。
另外,如果除了图片,还输出了文字,就会报错,下面就会讲述如果图文混排。
8. nodejs异常处理
即如果我们的路由出现问题时,后台就会崩溃,这就是异常。
处理异常的方式也很简单。
对于同步的方法就是使用try-catch语句,把有可能出错的代码放在try中,然后catch到错误后处理错误。也可以根据情况,在try失败时,throw出来,然后再catch。
而对于异步方法可以直接在自身的err下处理异常就可以了。 即对于可能使得后台崩溃的地方使用异常处理。
9. 异步流程控制
第一步:读文件, 第二步: 写入磁盘, 第三步: 入库。
在其他的后台语言中,这三步是一个执行完成另外一个再执行,但是在node中,由于默认所有的api都是异步的,所以这三个步骤是一起并发运行的。但是我们需要的是第一步完成后才能实用该结果运行第二步,第二步完成之后才能实用结果完成第三步。。。。
那么这个问题怎么解决呢? 在node中,往往最后一个参数是一个回调函数,所以可以在做完第一步之后再将第二步作为一个回调函数去执行,然后第二步执行完成之后,去完成作为第二步的回调函数的第三步。。。。这就是所谓了回调地狱了。
这只是三个步骤,如果去写嵌套,就会发现十分的不友好, 所以我们需要的是有一种处理机制来解决这个问题。
首先,我们分析一个多个函数运行可能会出现的情况:
- 串行无关联 --- 即一个执行完成之后再执行另一个,但是这几个之间的执行结果都不会被下一个利用。
- 并行无关联 --- 即所有的函数并行执行,谁先执行完是不知道的,关键是谁先执行完都对结果没有影响。
- 串行有关联 --- 即我们再上面的问题中所需要的一种执行模式。 一个执行完,才能执行下一个,并且这个执行完的结果是作为下一个函数执行所需要的。
- 并行限制 --- 即很多函数并行执行,但是我们可以限制每次并行执行的数量, 如限制每次只有2个函数可以并行执行。
前面三个是比较重要的,所以我们先学习前面三个,就要用到nodejs中的async。 但是这个不是node核心所包含的。所以我们需要在一个文件中引入async, 即:
npm install async --save-dev
但是不知是什么问题。。。 不能安装成功,所以还是使用淘宝的镜像吧。。。
cnpm install async --save-dev
安装完成之后,就可以发现package.json中已经包含了这个依赖项:
"devDependencies": { "async": "^2.3.0" }
我们首先看一下两个函数并行执行的情况:
function oneFun() { ii = 0; setInterval(function () { ii++; if (ii == 3) { clearInterval(this); } console.log("aaa" + new Date()); }); console.log("oneFun"); } function twoFun() { jj = 0; setInterval(function () { jj++; if (jj == 3) { clearInterval(this); } console.log("bbb" + new Date()); }); console.log("twoFun"); } oneFun(); twoFun(); console.log("执行完毕");
输出结果如下:
oneFun twoFun 执行完毕 aaaSat Apr 15 2017 11:21:40 GMT+0800 (中国标准时间) bbbSat Apr 15 2017 11:21:40 GMT+0800 (中国标准时间) aaaSat Apr 15 2017 11:21:40 GMT+0800 (中国标准时间) bbbSat Apr 15 2017 11:21:40 GMT+0800 (中国标准时间) aaaSat Apr 15 2017 11:21:40 GMT+0800 (中国标准时间) bbbSat Apr 15 2017 11:21:40 GMT+0800 (中国标准时间)
由于setInterval()是异步的,所以oneFun和twoFun很快就执行结束,然后由于两个setInterval异步,所以他们并发执行,可以看到他们是接替执行的,aaa、bbb、aaa、bbb、aaa、bbb。。。
注意:这里使用clearInterval(this);就可以清楚当前的计时器。
但是,如果我们希望在oneFun执行完了之后再执行twoFun呢? 由于setTimeout()是异步的,一般方法我们就只能用下面的方法来实现,如下:
function oneFun() { ii = 0; setInterval(function () { ii++; if (ii == 3) { clearInterval(this); twoFun(); } console.log("aaa" + new Date()); }); console.log("oneFun"); } function twoFun() { jj = 0; setInterval(function () { jj++; if (jj == 3) { clearInterval(this); } console.log("bbb" + new Date()); }); console.log("twoFun"); } oneFun(); console.log("执行完毕");
但是不难理解,这样的做法会很难管理,并且这还只是两个异步的控制,如果有多个,简直就会疯掉~
于是async就可以用得到了。。。
首先我们使用async完成下面的串行无关联,利用async.series。
var async = require("async"); function exec() { async.series( { one: function (done) { ii = 0; setInterval(function () { ii++; console.log("aaa" + new Date()); if (ii == 3) { clearInterval(this); done(null, "onefun执行完毕"); } }); }, two: function (done) { jj = 0; setInterval(function () { jj++; console.log("bbb" + new Date()); if (jj == 3) { clearInterval(this); done(null, "onefun执行完毕"); } }); }, three: function (done) { done(null, "所有程序执行完毕"); } },function (err, rs) { console.log(err); console.log(rs); } ); } exec();
即实现将async模块引入,然后利用async.series()方法,即串行无关联,这个方法接受两个参数,第一个参数是一个对象,对象中可以定义我们希望串行执行的方法,然后第二个参数是一个回调函数,这个回调函数要在第一个参数的方法中作为回调。 指的注意的是: 这里只有第一个参数的每一个方法直到执行完了done();并且done()的第一个参数是null(即没有出错),才会执行下一个方法, 这样就可以实现串行无关联了。
结果如下所示:
可以看到aaa全部打印出来之后,才打印的bbb,然后最后输出了一个对象说明了执行情况。
注意: 前面的one和two中的两个done是用来告知下一个方法:我执行完了,你可以执行了。 而最后一个done才是用来提示的,如果最后一个done没有回调,当然也可以串行执行,因为他是最后一个了,但是就不会返回这个对象了。
下面再介绍一下并行无关联,很简单,只需要将上面的async修改为parallel即可。最后的输出结果如下:
可以看到,a和b显然是并行执行的,从最后一句可以看到three是最先完成的,因为并行情况下执行时间最短的最先完成。
注意: 这里由于是并行执行,所以done()的作用就不是控制后续进程的进行了,而只是说明是否执行了。
最后,我们介绍一个 串行有关联 的实现, 即几个异步的函数,我们让他们串行执行,并且在一个执行完了之后将结果传输给下一个要执行的函数利用,如下所示:
var async = require("async"); function exec() { async.waterfall( [ function (done) { ii = 0; setInterval(function () { ii++; console.log("aaa" + new Date()); if (ii == 3) { clearInterval(this); done(null, "第一个函数向下传递值"); } }); }, function (preValue ,done) { jj = 0; setInterval(function () { jj++; console.log("bbb"+ "后面是接受的第一个函数的值 " + preValue); if (jj == 3) { clearInterval(this); done(null, "第二个函数向下传递值"); } }); }, function (preValue,done) { console.log("后面是第二个函数给我传递的值:" + preValue); done(null, "所有程序执行完毕"); } ],function (err, rs) { console.log(err); console.log(rs); } ); } exec();
注意点1:使用async.waterfall方法实现串行有关联
注意点2: 该方法的第一个参数是一个数组, 第二个参数是一个回调函数,没有变化。 由于这是数组,所以就没有键的概念了,故每一个元素都是一个函数。
注意点3: 由于这是串行有关联的,所以我们可以通过function的第一个参数来接收上一个函数执行完之后返回的结果。
10. 连接MySQL
连接MySQL的方法有两种,一种是直接连接,另一种是使用连接池连接。 直接连接更简单一些,所以我们先学习直接连接。
直接连接MySQL。
首先使用node安装在当前文件夹下,如下(使用npm安装会出现问题,所以使用cnpm):
cnpm install mysql
11. Buffer
在网络层对于不同的文件都是使用二进制交互的,而js本身不具有能力,所以在node中引入了Buffer用于引入。 对于Buffer,我们可以在命令行中查看。
直接输入Buffer,回车,可以看到Buffer是一个对象, 它也是一个构造函数: 如下:
我们可以new 一个Buffer,并且传入一个字符串,存入Buffer时,默认的编码格式是uft-8, 如下:
然后,我们可以指定其具体的编码格式,如base64, 如下:
我们还可以指定其长度,然后写入:
可以看到我们已经定义了buf的长度为7, 即使写的超过了7, 最后也只会留下7个,其他的被忽略掉。
我们可以使用Buffer.isBuffer()来判断是否是Buffer,如下:
Buffer写入,如果我们直接使用write()方法写入,即只接受一个字符串,那么会覆盖之前的, 如下所示:
但是write()方法还可以接受第二个参数,即偏移量,即从哪里开始写,如下所示:
从这里可以看出, 偏移量是指从0开始计算的。 并且一旦开始new的时候传入了字符串,就确定了buf的长度,后面即使是再写入,也不能超过原来的长度。
copy()方法如下所示:
即第二个参数指定开始写的位置,第三个参数指定复制的起始位置,第四个参数指定赋值的结束位置。
重要:
每次我们再修改了js内容时,都需要重新启动服务器,这是非常麻烦的,所以我们可以使用类似react中的热加载,即安装supervisor,如下:
npm install supervisor --save
然后就可以全局使用了, 接着创建一个node文件,原来我们是通过 node <文件名> 方式来执行的,现在,我们使用 supervisor --harmony <文件名> 的方式就能执行,并且只要我们改了js,并且保存,这时再刷新页面就会发现已经改变, 而不需要再次启动node服务器。
视频资源:http://study.163.com/course/courseLearn.htm?courseId=1003228034#/learn/video?lessonId=1003665724&courseId=1003228034
代码资源: http://www.yuankuwang.com