nodejs --- 重要知识点回顾

Posted Wayne Zhu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了nodejs --- 重要知识点回顾相关的知识,希望对你有一定的参考价值。

1. 运行一个nodejs文件

如一个js文件中只含有console.log("hello world");的文件,我们再git里运行node,即 node hello.js 即可发送输出hello world。如下:

 

2. 交互模式

直接输入node,即进入node环境,即可输入任何语句

 

 

3. 创建一个简单的服务器

创建server.js -> require http模块 -> 调用 createServer 方法进行创建 -> 监听某个端口 -> 运行js文件(实际上是在运行这个node服务器) -> 在浏览器中发送请求。

js文件如下:

其中比较重要的是要知道createServer这个内置函数就是用于创建服务器的,然后接受一个函数作为参数,我们可以用writeHead方法来写头部,使用end方法来输出内容。用listen来监听端口号。当然,也可以是监听8889等端口号,只要是合法的就行。在浏览器端发出请求,结果如下:

 

 

4. node中的npm

 

安装node时就已经安装了npm,即一个包管理工具,我们一般利用它来安装一些包,即npm install <package> 如果后面添加 -g ,那么就会将包安全到全局环境,即user路径下。如果不添加,就会安装在当前文件夹下,当然首先会创建node_modules文件,在此之下。

 

一般,我们要创建一个项目时,我们可以先npm init,通过它,我们就可以创建一个package.json文件,然后通过配置该文件说明我们的项目信息。其中主要的参数有:

(1)name --- 包名   (2)version --- 版本号  (3) description --- 包的描述   (4) homepage --- 包的官网url  (5) author --- 包的作者名字  (6) contributors --- 包的其他贡献者名字

(7)dependencies  --- 依赖包列表(如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下)    (8)repository --- 包代码存放的地方   (9)main --- main 字段是一个模块ID,它是一个指向你程序的主要项目。就是说,如果你包的名字叫 express,然后用户安装它,然后require("express")    (9) keywords --- 关键字

 

卸载模块: npm uninstall <package>

 

更新模块: npm update <package>

 

版本号相关: 当使用npm下载和发布代码时,都会涉及到版本号相关,即X-Y-Z,一般而言,只有当版本发生了重大的变化,不向下兼容时,X才会变化; 如果增加了新的功能,仍旧向下兼容 Y 发生变化; 如果仅仅是做了很小的改变,如修复了bug,那么 Z 发生变化。

 

如果希望知道某条命令的详细信息,如install的,可以输入 npm help install

 

npm连接的是国外的网站,也可以使用淘宝镜像:

npm install -g cnpm --registry=https://registry.npm.taobao.org

 然后就可以使用cnpm来安装了。

 

 

5. nodejs REPL

即node的交互式解释器,即我们输入node之后即进入node环境,这个node环境就是node的交互式解释器。

值得注意的是,和console控制台不同,REPL支持多行输入,如下所示:

$ node
> var x = 0
undefined
> do {
... x++;
... console.log("x: " + x);
... } while ( x < 5 );
x: 1
x: 2
x: 3
x: 4
x: 5
undefined
>

一般我们使用ctrl + c来退出该环境,大多数情况下,我们都是用ctrl+c来退出环境的。不论是否是node。

 

 

6. nodejs回调函数

在node中,我们知道其最大的特点就是异步I/O,而实现异步I/O的关键就在于回调函数。

首先,我们看看同步的是什么样:

var fs = require("fs");
var data = fs.readFileSync(\'test.txt\');
console.log(data.toString());
console.log("over");

这是main.js中的代码,即先引入fs模块,然后才能实用相应的API,readFileAsync()函数用于同步读取文件,它是阻塞的,返回这个读取的文件,输出如下:

 

下面这个一个异步的,

    var fs = require("fs");
    fs.readFile("test.txt",function (err, data) {
        if (err) {
            console.log(err);
        } else {
            console.log(data.toString());
        }
    });
    console.log("over");

结果如下:

即readFile是一个异步的,在执行这条语句的时候不会阻塞后面的语句,而是在读完文件之后再console。

即我们在读取代码的时候可以做下面的很多事情,这样就可以节省很多时间。

readFile和readFileSync都接受第二个参数,即编码类型,如"utf-8"

 

 

 

7. nodejs事件循环

nodejs事件循环利用的是观察者模式,也就是发布订阅模式。简单的理解,DOM元素绑定事件就是这样的模式。其中绑定的元素是发布者,函数是订阅者,当元素发生了变化时(被点击等),就会通知所有的订阅者。 

nodejs使用事件驱动模型,当服务器接受到了请求之后,就会关闭这个请求,然后再处理,为的是等待下一个请求。这样,请求就不会被耽搁。这个模型的效率非常高,因为他一直在接受请求,而没有等待任何读写操作。在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数:

虽然这里没有所谓的DOM元素,但是实现应当是一样的,即观察者模式的最好理解是好莱坞电影中的一句话: 你不要打电话给我,我会打电话给你

在node中我们常常使用events模块来实现,即首先引入events,然后创建一个对象,利用这个对象的on方法绑定时间,利用对象的emit方法来触发事件,如下所示:

var events = require("events");
var eventEmitter = new events.EventEmitter();
eventEmitter.on("selfDefine", function () {
    console.log("This is selfDefine1");
});
eventEmitter.on("selfDefine", function () {
    console.log("This is selfDefine2");
});
eventEmitter.on("selfDefine", function () {
    console.log("This is selfDefine3");
});
eventEmitter.on("selfDefine", function () {
    console.log("This is selfDefine4");
});
eventEmitter.emit("selfDefine");
console.log("over");

最终效果如下:

在这里,我们就可以认为这是发布订阅者模式,首先可以知道发布者是selfDefine事件,订阅者是4个,一旦selfDefine被触发,那么就会通知订阅者,方法是将订阅者添加到了Event Loop中去,然后通过事件循环来监听,一旦被触发,就会通知,即我给你打电话,你没有给我打电话。

当事件触发时,注册到这个事件的事件监听器被依次调用。

 

另外,在on和emit中是可以传递参数的,如下所示:

var events = require("events");
var eventEmitter = new events.EventEmitter();
eventEmitter.on("selfDefine", function (x) {
    console.log("This is selfDefine1 " + x);
});
eventEmitter.on("selfDefine", function (x) {
    console.log("This is selfDefine2 " + x);
});
eventEmitter.on("selfDefine", function (x) {
    console.log("This is selfDefine3 " + x);
});
eventEmitter.on("selfDefine", function (x) {
    console.log("This is selfDefine4 " + x);
});
eventEmitter.emit("selfDefine", "argument");
console.log("over");

最终的效果如下:

 

 

关于 EventEmitter 还有其他的属性,如下所示:

addListener(event, listener) --- 它和on是类似的,都是添加某一个事件的监听器

removeListener(event, listener) --- 即通过此API可以将监听器取消(特定的listener)。

removeAllListeners(event) --- 可以取消event下的所有监听器。

newListener(event, listener); --- 该事件在添加新的监听器时被触发。

listenerCount(emitter, event); --- 返回指定监听器的数量。

listeners(event) --- 返回指定事件的监听器数组。

 

once(event, listener) --- 通过once就可以知道,这个监听器只会监听一次,后面再调用,就不会监听了。

举例如下:

var events = require("events");
var eventEmitter = new events.EventEmitter();
eventEmitter.on("foo", function () {
    console.log("via on");
});
eventEmitter.once("foo", function () {
    console.log("via once");
});
eventEmitter.emit("foo");
setTimeout(function () {
    eventEmitter.emit("foo");
}, 1000);

 

最终的执行效果如下:

在执行过程中vai on和via once是同时出现的,过了1s之后,via on 出现, via once不再出现,因为通过once添加的监听器只会监听一次,然后就被销毁了(即后面不再监听)。

 

 

 

 

8. Nodejs Buffer (缓冲区)

作为服务器端语言的nodejs,自然会接受请求,如TCP请求,都是通过二进制来传递的,但是js语言本身并没有接受二进制的api,所以nodejs中添加了Buffer类来作为存储二进制数据的缓冲区。

通过Buffer类创建buffer实例的几种方法:
1. 创建长度为10字节(1 B = 8 bit)的Buffer类, var buf = new Buffer(10);

2. 通过数组创建Buffer实例, var buf = new Buffer([10, 20, 30, 15]);

3. 通过一个字符串来创建buffer实例, var buf = new Buffer("i love coding", "utf-8"); 注意: 我们这里使用utf-8格式编码,还可以是"ascii",  "utf16le", "ucs2", "base64" 和 "hex",当然,默认就是utf-8。

已经有了buffer实例,我们就可以使用buffer实例的一些方法了,如下所示:

  • write()(写入数据) --- buf.write(string[, offset[, length]][, encoding])。它的返回值是写入的长度。  我们知道[]表示式可选的, 其中string是将要写入的字符串; offset是缓冲区开始写入的索引值,默认为0;length是长度,默认是buf.length; encoding是编码方式,默认是utf-8。
  • toString() (读取数据)--- buf.toString([encoding[, start[, end]]])。它的返回值是读取的值。其中的encoding表示读取数据的编码方式, start和end表示读取数据的位置。
  • toJSON() (转换为JSON对象)--- buf.toJSON(buf)。 返回值是一个JSON对象。
  • Buffer.concat(list[,totalLength]) (合并Buffer对象) --- 返回值是合并后的buffer对象。其中list是一个数组,其中的每个元素是一个buffer实例,totalLength是在制定合并之后的总长度。
  • buf.compare(otherBuffer) (缓冲区大小比较) ---  比较两个缓冲区的大小,返回 0 -1 1 。
  • buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]]) (缓冲区的拷贝
  • buf.slice([start[, end]])(缓冲区的裁剪)
  • buf.length() --- 返回缓冲区的长度。
  • ......

 

9. Nodejs Stream(流)

  Stream是一个抽象的接口,并且它是eventEmitter的实例,通常Stream有四种流类型, 包括可读、可写、可读可写、操作被写入然后读出。

  既然它是eventEmitter的实例,那么就会有事件,这个事件当然不再是自定义然后使用emit的方式,而是已经定义的,不再需要emit了。 有 data 、end、error、finish。其中data表示只要发现有数据就会立即触发, end表示没有更多的数据可读时就会触发, error是在读或写发生错误的时候触发,finish在所有的数据被写入底层系统时触发。

  因为流中的读写都是与文件相关,所以需要引入fs模块。读:

var fs = require("fs");
var readStream = fs.createReadStream("./test.txt");
readStream.setEncoding("utf-8");
var data = "";
readStream.on("data", function (chunk) {
    data += chunk;
});
readStream.on("end", function () {
    console.log(data +" FINISHED");
});
readStream.on("error", function (err) {
    console.log(err);
});

  可以看到首先引入文件系统fs,然后使用 createReadStream()方法来读取文件, 绑定了data之后,只要文件中有内容就会被触发,当读取文件内容结束之后,就会执行end下的监听器。 在读的过程中有错,就会执行error下的监听器。

  

  下面是写操作:(其中的test.js现在内容为空)

var fs = require("fs");
var writeStream = fs.createWriteStream("./test.txt");
var data =  "I want to write something";
writeStream.write(data);
writeStream.end();
writeStream.on("finish", function () {
    console.log("finished");
});
writeStream.on("error", function (err) {
    console.log(err);
});
console.log("ok!");

 

   最终输出为: ok!   finished

  注意: 我们需要使用end()方法表示结束,然后当写入完成之后就会触发finish, 最后,我们打开test.txt就会发现确实已经写入了data数据 。

 

管道流:它提供了这样的一个机制 --- 从一个流中读取数据,然后输入到另一个流中。

var fs = require("fs");
var readStream = fs.createReadStream("./input.txt");
var writeStream = fs.createWriteStream("./output.txt");
readStream.pipe(writeStream);
console.log("finished");

 

 

通过这种方式,我们就可以将input.txt中的内容流到output.txt之中了。

 

链式流:链式是通过连接输出流到另外一个流并创建多个对个流操作链的机制。链式流一般用于管道操作。

引入zlib模块进行压缩文件, 之所以说是链式流,是因为我们可以连续使用pipe(),如下所示:

var fs = require("fs");
var zlib = require("zlib");
var readStream = fs.createReadStream("./foo.txt");
readStream.pipe(zlib.createGzip()).pipe(fs.createWriteStream("./foo.min.txt"));

 

即我们首先引入fs模块和压缩库zlib,然后再创建一个读流,通过管道流pipe到压缩文件,然后再pipe到一个将要保存压缩文件的写文件。最后可以看到foo.min.txt是被压缩过的。(注意:其中foo.min.txt不需要自己来写,他会自动生成)

当然我们还可以用createGunzip()方法来解压缩。

 

 

 

10. nodejs模块系统

一个nodejs文件就是一个模块。我们之前使用var fs = require("fs"); 这里就是引入了一个fs模块。因为fs是内置的,所以直接引入就好。但是如果是我们自己创建的一个foo.js文件,我们希望引用这个模块,就可以使用var foo = require("./foo"); 即引入当然文件目录下的foo.js中的模块。 其中js是默认的,省略不写。

我们接触到的require是nodejs提供的一个接受对象,与之相对的是exports倒出对象。

比如我们创建一个foo.js,这就是一个模块,内容如下:

function Foo() {
    this.sayHello = function () {
        console.log("hello world!");
    }
}
module.exports = Foo;

 

即这导出了一个构造函数。 然后我们就可以引入这个module了,如下:

var Foo = require("./foo");
var myFoo = new Foo();
myFoo.sayHello()

 

这里我们require到了这个模块,然后创建了实例,调用了模块的方法。

 

服务器端的模块:之前我们使用的require("http")就是在引入服务器端的模块。 然后再直接调用即可,如createServer()方法。

nojs加载模块方式如下:

第一步: 判断文件模块缓存区中是否存在模块。(对于我们之前加载过的模块,会缓存到缓存区中,下次最先查找并加载)。

第二步: 判断是否是原生模块,如http、fs等这就是原生模块。 如果判断一个模块是原生模块就会优先加载原生模块。即即使我们有了一个http.json文件,但是由于原生模块的优先级更高,所以优先加载。

第三步: 判断是否是自定义的模块。 即我们自己设定的模块。

 

 

 

 

11. nodejs函数

  nodejs中的函数也可以作为另一个函数的参数,同样也有匿名函数的概念,如下所示:

var http = require("http");
http.createServer(function (request, response) {
    response.writeHead("200", {"Content-Type": "text/plain"});
    response.write("zhuzhenwei,you are handsome!");
    response.end();
}).listen(8888);

  这样就创建了一个服务器。

  注意:其中在writeHead中最好写上charset=utf-8; 后面的字符也可以是utf8、UTF8、UTF-8。 他们都是有效的的。

  例如下面的这个函数,和js中的作用域是一样的,注意其中我们需要将Content-Type的内容写成text/plain; charset=utf8; 否则在浏览器中输出汉字时会有问题。

var http = require("http");
http.createServer(function (req, res) {
            res.writeHead(200, {"Content-Type": "text/html; charset=utf8"});
            if (req.url !== "/favicon.ico") { // 清除第二次访问
                console.log("访问");
                a(res);
                res.write("Hello world!");
                res.end();  
            }
}).listen(8081);
function a(res) {
    res.write("hello, 我是一个被调用的函数。");
}

console.log("Server running at http:127.0.0.1:8081");

  其中的req.url !== "/favicon.ico"是为了解决自身的bug的。 

 

 

上面的这种方式是对于一个内部的函数而言的,但是如果我们希望是一个外部文件的函数应该怎么办呢? 

如下,建立一个fun2.js,和server.js在同一个文件下,内容如下:

function a2(res) {
    res.write("我是fun2调用的函数");
}
module.exports = a2;

注意:最后一句的意思是我们希望将这个函数导出,如果不导出去,就没有办法使用。然后server.js内容如下:

var http = require("http");
var otherfun = require("./fun2");
http.createServer(function (req, res) {
            res.writeHead(200, {"Content-Type": "text/html; charset=utf8"});
            if (req.url !== "/favicon.ico") { // 清除第二次访问
                console.log("访问");
                otherfun(res);
                res.write("Hello world!");
                res.end();  
            }
}).listen(8081);


console.log("Server running at http:127.0.0.1:8081");

注意: 其中如果我们要使用这个函数,就必须要用otherfun来调用,虽然在fun2中的函数名是fun2,但是在server.js中只认otherfun。 

  另外,因为这是一个本地的文件,我们最好在前面加上./ 表示相对位置。

缺点:可以看到这样,我们每次只能在一个文件中导出一个函数,但是对于一个文件中有多个函数的情况应该怎么导出呢? 如下所示:

module.exports = {
    func2 : function (res) {
        res.write("我是func2函数");
    },
    func3 : function (res) {
        res.write("我是func3函数");
    }
};

即将函数使用对象的形式定义,然后我们导出这么个对象,就向http一样,我们引入这个http对象之后,然后使用http.createServer等方法。 

调用的时候显然就是下面这样的,调用对象的方法:

var http = require("http");
var fun = require("./fun.js");
http.createServer(function (req, res) {
            res.writeHead(200, {"Content-Type": "text/html; charset=utf8"});
            if (req.url !== "/favicon.ico") { // 清除第二次访问
                console.log("访问");
                fun.func2(res);
                fun.func3(res);
                res.end();  
            }
}).listen(8081);


console.log("Server running at http:127.0.0.1:8081");

效果如下:

所以,可以看到,后者可以调用多个函数,一般就用后面这种形式。

另外,我们还经常用字符串的形式,如fun["func2"]或者fun["func3"],这样的好处是:我们可以把调用写活了,在后面讲到路由的时候更为重要,因为可以把字符串作为一个变量,输入不同的值,就可以调用不同的函数, 非常重要。。

 

  

12. nodejs路由(重点)

我们所需要的数据都在request对象中,另外,我们得先解析url,需要引入url模块和querystring模块。

即url.parse()可以解析这个url。

对于客户端输入的url,我们通过request.url即可获取。

这里比较难理解。。。

 

13. nodejs全局对象

与浏览器中window作为全局不同,在node中global是全局对象,我们可以直接在全局对象上定义属性,那么就可以访问到了,指的注意的是,由于每一个nodejs模块都是一个作用域,所以直接var是局部变量,而不是全局变量。

__filename --- 这个全局变量表示运行的nodejs的文件名。

__dirname --- 表示目录名称(不包含文件名)

在文件中的代码如下:

console.log(__filename);
console.log(__dirname);

 

输出如下:

可以看出,其中__filename是包含了路径的。 而__dirname仅仅是缺少了文件名,只有路径。

 

另外,console、setTimeout、clearInterval等等都是全局对象,举例如下:

console.time("set");
var Timer = setInterval(function () {
    console.log(__dirname);
}, 1000);
setTimeout(function () {
    clearInterval(Timer);
    console.log("cleared");
},5000);
console.timeEnd("set");
console.info("info");
console.warn("warn");
console.log("my birthday is %d", 19950628);

 

最终的输入如下:

可以看到,由于setInterval和setTimeout是非阻塞的,所以后面的语句先执行,指的注意的是其中的console.log("%d",19950628)的应用,这与C语言中的printf是非常相似的。另外console.trace()可以追踪调用栈。

另外还有一个比较重要的api,即setImmediate(handler); 他是IE10中支持的。其他的浏览器一律不支持,但是node是支持的,这个解决单线程阻塞的问题,当然用setTimeout(handler, 0);也可以,但是后者的延迟时间较前者更长一些。

 

process也是一个全局变量,在node环境下输入global.process就可以看到其中具有的变量,因为process本身就是一个对象。不难理解process是描述进程(process即进程的意思)的一个全局对象。

他还有一些事件,如下:

举例如下:

setImmediate(function () {
    console.log("god");
});
process.on("exit", function (code) {
    console.log("exitCode is:", code);
});
console.log("over");

 

 

即进程一旦结束,就会触发监听器。 注意:这里使用on的方式,所以我们可以认为process是eventEmitter的实例。

输入如下:

退出码为0是什么意思呢?  因为每当exit事件触发,都会有一个code即退出码,表示这个退出的方式,0表示正常退出,一般还有如下几种退出码:

 

process 不仅提供了上述事件,还提供了非常多的有关进程的属性,如pid(进程号)、platform(程序运行的平台)、archf(当前CPU的架构)、title(进程名,默认为node)、versions(包含了node的版本和依赖)、version(node的版本)、execPath(当前脚本的二进制文件路径)、stdin、stdout、stderr。举例如下:

console.log(process.platform);
console.log(process.version);
process.stdout.write("hello world \\n");
console.log(process.execPath);

 

注意:其中的stdout.write是在终端输出,那么什么时候才能在页面上输出呢?  显然,由于node是服务器端语言,所以说只能通过响应(response)的方式才能返回给客户端。

 

下面的process方法也是常用的:

console.log(process.memoryUsage());
console.log(process.cwd());

 

其中第一个是内存使用情况。后者是cwd(current working directory)即当前工作目录。

可以看到rss、heapTotal、heapUsed表示了内存使用情况。

 

 

 

14. nodejs常用工具

这里需要介绍的工具,首先要引入util模块,主要有util.inherits、util.inspect、util.isArray(object)、util.isRegExp(object)、util.isDate(object)、util.isError(object)。

 

util.inherits(subconstructor, supconstructor),

即这个方法可以实现继承,但是这里的继承和我们使用js实现的继承也有不同之处,主要区别是这里的继承是原型对象之间的继承,而不会继承上一级的构造函数,举例如下:

var util = require("util");
function Sup() {
    this.name = "sup";
    this.sayHello = function () {
        console.log(this.name);
    }
}
Sup.prototype.show = function () {
    console.log("just show yourself");
}
function Sub() {
    this.name = "sub";
}
util.inherits(Sub, Sup);
var objSup = new Sup();
console.log(objSup.name);
var objSub = new Sub();
objSub.show();
// objSub.sayHello(); // 报错, objSub.sayHello() is not a function.

 

这里可以看到其中的objSub.show()成功继承了Sup的原型中的方法,但是objSub.sayHello()却会报错,因为通过util.inherits()的方式是不能继承构造函数中的方法的,与js中的不一样,需要注意。

 

util.inspect(obj)

这里并不是只有一个参数,它实际上还可以接受三个,第二个是true/false,表示是否显示更多的信息,第三个是depth,即递归的层数,默认是2层; 第四个是关于颜色的true/false。举例如下:

var util = require("util");
function Foo() {
    this.age = 21;
    this.name = \'zzw\';
    this.sayHello = function () {
        console.log(this.age);
    }
}
console.log(util.inspect(Foo));
console.log(util.inspect(Foo, true));

 

最终的输出如下:

可以看到,没有第二个参数,那么只会输出简单的一个函数,如果为true,就会输出更多深层次的内容。

 

 util.isArray()

实际上,这里和js中的Array.isArray()是一样的,举例如下:

var util = require("util");
console.log(util.isArray([])); //true
console.log(util.isArray(new Array()));//true
console.log(util.isArray({}));//true

 

 

util.isRegExp() 即判断是否是一个正则表达式

 

util.isDate() 即判断是否是一个日期

 

util.isError() 即判断是否是一个错误对象

 

 

15. nodejs 文件系统

nodejs作为后台语言,必然不可避免的需要和数据库文件等打交道,所以文件系统模块是非常必要的。即file system --- 文件系统。

在node中的fs中,所有的方法均有同步和异步之分。如同步读取文件,fs.readFile();异步读取文件,fs.readFileSync()。如下所示:

var fs = require("fs");

// 异步读取
fs.readFile(\'input.txt\', function (err, data) {
   if (err) {
       return console.error(err);
   }
   console.log("异步读取: " + data.toString());
});

// 同步读取
var data = fs.readFileSync(\'input.txt\');
console.log("同步读取: " + data.toString());

console.log("程序执行完毕。");

 

ok!  下面介绍一些node中常用的文件操作api。

打开文件

var fs = require("fs");
console.log("准备打开文件");
fs.open("foo.txt","r+",function (err, fd) {
    if (err) {
        return  console.log(err);
    } 
    console.log("成功打开文件");  

});

 

即第一个参数是要打开的文件的路径,第二个参数是打开的flag(方式),第三个参数是回调函数,即如果打开错误,返回输出错误,否则输出成功打开文件。

其中flag有下面的方式: 

  • r 读模式打开文件
  • r+  读写模式打开文件
  • rs 同步方式打开文件
  • rs+ 同步方式打开和读写文件
  • w  写入方式打开文件
  • w+ 读写方式打开文件,如果没有,就创建
  • 。。。

 

获取文件的相关信息

举例如下:

输入如下所示: