nodejs基础知识查缺补漏

时间:2022-06-24 14:15:23

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(, {"Content-Type": "text/html"});
console.log("访问");
res.write("Hello world!");
res.end();
}).listen(); 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(, {"Content-Type": "text/html"});
if (req.url !== "/favicon.ico") { // 清除第二次访问
console.log("访问");
res.write("Hello world!");
          res.end();
}
}).listen(); 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++一样,有公有的、私有的、受保护的等等。 整体的架构如下:

nodejs基础知识查缺补漏

其中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(, {"Content-Type": "text/html; charset=utf8"});
if (req.url !== "/favicon.ico") {
var student = new Student(, "John Zhu", );
console.log(student.enter());
student.learn(res);
res.end();
}
}).listen(); 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(, {"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(); 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(, {"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(); 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>

最终运行输出如下:

nodejs基础知识查缺补漏

为什么"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(, {"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(); 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 = ;
setInterval(function () {
ii++;
if (ii == ) {
clearInterval(this);
}
console.log("aaa" + new Date());
});
console.log("oneFun");
}
function twoFun() {
jj = ;
setInterval(function () {
jj++;
if (jj == ) {
clearInterval(this);
}
console.log("bbb" + new Date());
});
console.log("twoFun");
}
oneFun();
twoFun();
console.log("执行完毕");

输出结果如下:

oneFun
twoFun
执行完毕
aaaSat Apr :: GMT+ (中国标准时间)
bbbSat Apr :: GMT+ (中国标准时间)
aaaSat Apr :: GMT+ (中国标准时间)
bbbSat Apr :: GMT+ (中国标准时间)
aaaSat Apr :: GMT+ (中国标准时间)
bbbSat Apr :: GMT+ (中国标准时间)

由于setInterval()是异步的,所以oneFun和twoFun很快就执行结束,然后由于两个setInterval异步,所以他们并发执行,可以看到他们是接替执行的,aaa、bbb、aaa、bbb、aaa、bbb。。。

注意:这里使用clearInterval(this);就可以清楚当前的计时器。

但是,如果我们希望在oneFun执行完了之后再执行twoFun呢? 由于setTimeout()是异步的,一般方法我们就只能用下面的方法来实现,如下:

function oneFun() {
ii = ;
setInterval(function () {
ii++;
if (ii == ) {
clearInterval(this);
twoFun();
}
console.log("aaa" + new Date());
});
console.log("oneFun");
}
function twoFun() {
jj = ;
setInterval(function () {
jj++;
if (jj == ) {
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 = ;
setInterval(function () {
ii++;
console.log("aaa" + new Date());
if (ii == ) {
clearInterval(this);
done(null, "onefun执行完毕");
}
});
},
two: function (done) {
jj = ;
setInterval(function () {
jj++;
console.log("bbb" + new Date());
if (jj == ) {
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(即没有出错),才会执行下一个方法, 这样就可以实现串行无关联了。

结果如下所示:
 nodejs基础知识查缺补漏

可以看到aaa全部打印出来之后,才打印的bbb,然后最后输出了一个对象说明了执行情况。

注意: 前面的one和two中的两个done是用来告知下一个方法:我执行完了,你可以执行了。 而最后一个done才是用来提示的,如果最后一个done没有回调,当然也可以串行执行,因为他是最后一个了,但是就不会返回这个对象了。

下面再介绍一下并行无关联,很简单,只需要将上面的async修改为parallel即可。最后的输出结果如下:

nodejs基础知识查缺补漏

可以看到,a和b显然是并行执行的,从最后一句可以看到three是最先完成的,因为并行情况下执行时间最短的最先完成。

注意: 这里由于是并行执行,所以done()的作用就不是控制后续进程的进行了,而只是说明是否执行了。

最后,我们介绍一个 串行有关联 的实现, 即几个异步的函数,我们让他们串行执行,并且在一个执行完了之后将结果传输给下一个要执行的函数利用,如下所示:

var async = require("async");

function exec() {
async.waterfall(
[
function (done) {
ii = ;
setInterval(function () {
ii++;
console.log("aaa" + new Date());
if (ii == ) {
clearInterval(this);
done(null, "第一个函数向下传递值");
}
});
},
function (preValue ,done) {
jj = ;
setInterval(function () {
jj++;
console.log("bbb"+ "后面是接受的第一个函数的值 " + preValue);
if (jj == ) {
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的第一个参数来接收上一个函数执行完之后返回的结果。

nodejs基础知识查缺补漏

10. 连接MySQL

连接MySQL的方法有两种,一种是直接连接,另一种是使用连接池连接。  直接连接更简单一些,所以我们先学习直接连接。

直接连接MySQL

首先使用node安装在当前文件夹下,如下(使用npm安装会出现问题,所以使用cnpm):

cnpm install mysql

11. Buffer

在网络层对于不同的文件都是使用二进制交互的,而js本身不具有能力,所以在node中引入了Buffer用于引入。 对于Buffer,我们可以在命令行中查看。

直接输入Buffer,回车,可以看到Buffer是一个对象, 它也是一个构造函数: 如下:

nodejs基础知识查缺补漏

我们可以new 一个Buffer,并且传入一个字符串,存入Buffer时,默认的编码格式是uft-8, 如下:

nodejs基础知识查缺补漏

然后,我们可以指定其具体的编码格式,如base64, 如下:

nodejs基础知识查缺补漏

我们还可以指定其长度,然后写入:

nodejs基础知识查缺补漏

可以看到我们已经定义了buf的长度为7, 即使写的超过了7, 最后也只会留下7个,其他的被忽略掉。

我们可以使用Buffer.isBuffer()来判断是否是Buffer,如下:

nodejs基础知识查缺补漏

Buffer写入,如果我们直接使用write()方法写入,即只接受一个字符串,那么会覆盖之前的, 如下所示:

nodejs基础知识查缺补漏

但是write()方法还可以接受第二个参数,即偏移量,即从哪里开始写,如下所示:

nodejs基础知识查缺补漏

从这里可以看出, 偏移量是指从0开始计算的。 并且一旦开始new的时候传入了字符串,就确定了buf的长度,后面即使是再写入,也不能超过原来的长度。

copy()方法如下所示:

nodejs基础知识查缺补漏

即第二个参数指定开始写的位置,第三个参数指定复制的起始位置,第四个参数指定赋值的结束位置。

重要

  每次我们再修改了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