Node.js介绍与安装
Node是JavaScript语言的服务器运行环境。
所谓“运行环境”有两层意思:首先,JavaScript语言通过Node在服务器运行,在这个意义上,Node有点像JavaScript虚拟机;其次,Node提供大量工具库,使得JavaScript语言与操作系统互动(比如读写文件、新建子进程),在这个意义上,Node又是JavaScript的工具库。
Node内部采用Google公司的V8引擎,作为JavaScript语言解释器;通过自行开发的libuv库,调用操作系统资源。
下载LTS版本
使用
安装完成后,运行node.js程序,就是使用node命令读取JavaScript脚本。
当前目录的demo.js脚本文件,可以这样执行。
$ node demo
# 或者
$ node demo.js
使用-e参数,可以执行代码字符串。
$ node -e 'console.log("Hello World")'
Hello World
在命令行键入node命令,后面没有文件名,就进入一个Node.js的REPL环境(Read–eval–print loop,”读取-求值-输出”循环),可以直接运行各种JavaScript命令。
$ node
> 1+1
2
>
REPL是Node.js与用户互动的shell,各种基本的shell功能都可以在里面使用,比如使用上下方向键遍历曾经使用过的命令。
特殊变量下划线(_)表示上一个命令的返回结果。
> 1 + 1
2
> _ + 1
3
在REPL中,如果运行一个表达式,会直接在命令行返回结果。如果运行一条语句,就不会有任何输出,因为语句没有返回值。
> x = 1
1
> var x = 1
命令行脚本
node脚本可以作为命令行脚本使用。
$ node foo.js
上面代码执行了foo.js脚本文件。
foo.js文件的第一行,如果加入了解释器的位置,就可以将其作为命令行工具直接调用。
#!/usr/bin/env node
调用前,需更改文件的执行权限。
$ chmod u+x foo.js
$ ./foo.js arg1 arg2 ...
作为命令行脚本时,console.log用于输出内容到标准输出,process.stdin用于读取标准输入,child_process.exec()用于执行一个shell命令。
异步操作
Node采用V8引擎处理JavaScript脚本,最大特点就是单线程运行,一次只能运行一个任务。这导致Node大量采用异步操作(asynchronous operation),即任务不是马上执行,而是插在任务队列的尾部,等到前面的任务运行完后再执行。
由于这种特性,某一个任务的后续操作,往往采用回调函数(callback)的形式进行定义。
var isTrue = function(value, callback) {
if (value === true) {
callback(null, "Value was true.");
}
else {
callback(new Error("Value is not true!"));
}
}
上面代码就把进一步的处理,交给回调函数callback。
Node约定,如果某个函数需要回调函数作为参数,则回调函数是最后一个参数。另外,回调函数本身的第一个参数,约定为上一步传入的错误对象。
var callback = function (error, value) {
if (error) {
return console.log(error);
}
console.log(value);
}
如果没有发生错误,回调函数的第一个参数就传入null。这种写法有一个很大的好处,就是说只要判断回调函数的第一个参数,就知道有没有出错,如果不是null,就肯定出错了。另外,这样还可以层层传递错误。
全局对象和全局变量
Node提供以下几个全局对象,它们是所有模块都可以调用的。
- global:表示Node所在的全局环境,类似于浏览器的window对象。需要注意的是,如果在浏览器中声明一个全局变量,实际上是声明了一个全局对象的属性,比如var x = 1等同于设置window.x = 1,但是Node不是这样,在模块文件中,声明var x = 1,该变量不是global对象的属性,global.x等于undefined。这是因为模块的全局变量都是该模块私有的,其他模块无法取到。
- process:该对象表示Node所处的当前进程,允许开发者与该进程互动。
- console:指向Node内置的console模块,提供命令行环境中的标准输入、标准输出功能。
Node还提供一些全局函数。
- setTimeout():用于在指定毫秒之后,运行回调函数。该方法返回一个整数,代表这个新建定时器的编号。
- clearTimeout():用于终止一个setTimeout方法新建的定时器。
- setInterval():用于每隔一定毫秒调用回调函数。该方法返回一个整数,代表这个新建定时器的编号。
- clearInterval():终止一个用setInterval方法新建的定时器。
- require():用于加载模块。
- Buffer():用于操作二进制数据。
Node提供两个全局变量,都以两个下划线开头。
- __filename:指向当前运行的脚本文件名。
- __dirname:指向当前运行的脚本所在的目录。
除此之外,还有一些对象实际上是模块内部的局部变量,指向的对象根据模块不同而不同,但是所有模块都适用,可以看作是伪全局变量,主要为module, module.exports, exports等。
模块化结构
前言: 虽然nodejs支持ES6模块化, 但大部分包是用的还是CommonJS规范, 所以这里有必要给大家说一下
Node.js采用模块化结构,按照CommonJS规范定义和使用模块。模块与文件是一一对应关系,即加载一个模块,实际上就是加载对应的一个模块文件。
require命令用于指定加载模块,加载时可以省略脚本文件的后缀名。
var circle = require('./circle.js');
// 或者
var circle = require('./circle');
require方法的参数是模块文件的名字。它分成两种情况,第一种情况是参数中含有文件路径(比如上例),这时路径是相对于当前脚本所在的目录,第二种情况是参数中不含有文件路径,这时Node到模块的安装目录,去寻找已安装的模块(比如下例)。
var fs = require('fs');
模块一旦被加载以后,就会被系统缓存。如果第二次还加载该模块,则会返回缓存中的版本,这意味着模块实际上只会执行一次。如果希望模块执行多次,则可以让模块返回一个函数,然后多次调用该函数。
核心模块
如果只是在服务器运行JavaScript代码,用处并不大,因为服务器脚本语言已经有很多种了。Node.js的用处在于,它本身还提供了一系列功能模块,与操作系统互动。这些核心的功能模块,不用安装就可以使用,下面是它们的清单。
- http:提供HTTP服务器功能。
- url:解析URL。
- fs:与文件系统交互。
- querystring:解析URL的查询字符串。
- child_process:新建子进程。
- util:提供一系列实用小工具。
- path:处理文件路径。
- crypto:提供加密和解密功能,基本上是对OpenSSL的包装。
自定义模块
Node模块采用CommonJS规范。只要符合这个规范,就可以自定义模块。
下面是一个最简单的模块,假定新建一个foo.js文件,写入以下内容。
module.exports = function(x) {
console.log(x);
};
上面代码就是一个模块,它通过module.exports变量,对外输出一个方法。
这个模块的使用方法如下。
// index.js
var m = require('./foo');
m("这是自定义模块");
上面代码通过require命令加载模块文件foo.js(后缀名省略),将模块的对外接口输出到变量m,然后调用m。这时,在命令行下运行index.js,屏幕上就会输出“这是自定义模块”。
module变量是整个模块文件的顶层变量,它的exports属性就是模块向外输出的接口。如果直接输出一个函数(就像上面的foo.js),那么调用模块就是调用一个函数。但是,模块也可以输出一个对象。下面对foo.js进行改写。
// foo.js
var out = new Object();
function p(string) {
console.log(string);
}
out.print = p;
module.exports = out;
上面的代码表示模块输出out对象,该对象有一个print属性,指向一个函数。下面是这个模块的使用方法。
// index.js
var m = require('./foo');
m.print("这是自定义模块");
CommonJS规范
Node 应用由模块组成,采用 CommonJS 模块规范。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
上面代码中,变量x和函数addX,是当前文件example.js私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为global对象的属性。
global.warning = true;
上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
上面代码通过module.exports输出变量x和函数addX。
require方法用于加载模块。
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
CommonJS模块的特点如下。
所有代码都运行在模块作用域,不会污染全局作用域。 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。 模块加载的顺序,按照其在代码中出现的顺序。
module对象
Node内部提供一个Module构建函数。所有模块都是Module的实例。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
每个模块内部,都有一个module对象,代表当前模块。它有以下属性。
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
下面是一个示例文件,最后一行输出module变量。
// example.js
var fss = require('fs');
exports.fss = fss;
console.log(module);
执行这个文件,命令行会输出如下信息。
{
id: '.',
path: '/Users/luwnto/Desktop',
exports: {
fss: {
...
parent: null,
filename: '/Users/luwnto/Desktop/a.js',
...
如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。
module.exports属性
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
exports变量
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
exports = function(x) {console.log(x)};
上面这样的写法是无效的,因为exports不再指向module.exports了。
下面的写法也是无效的。
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
module.exports = function (x){ console.log(x);};
如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
异常处理
Node是单线程运行环境,一旦抛出的异常没有被捕获,就会引起整个进程的崩溃。所以,Node的异常处理对于保证系统的稳定运行非常重要。
一般来说,Node有三种方法,传播一个错误。
- 使用throw语句抛出一个错误对象,即抛出异常。
- 将错误对象传递给回调函数,由回调函数负责发出错误。
- 通过EventEmitter接口,发出一个error事件。
try…catch结构
最常用的捕获异常的方式,就是使用try…catch结构。但是,这个结构无法捕获异步运行的代码抛出的异常。
try {
setTimeout(function(){
throw new Error("error");
},1000)
} catch (err) {
//can not catch it
console.log(err);
}
无法被catch代码块捕获,因为catch代码块所在的那部分已经运行结束了。
一种解决方法是将错误捕获代码,也放到异步执行。
setTimeout(function() {
try {
if (true)
throw new Error("woops!");
else
console.log("Ok");
} catch(e) {
console.log(e);
}
}, 2000)
这种处理方法都不太理想。一般来说,Node只在很少场合才用try/catch语句,比如使用JSON.parse解析JSON文本。
回调函数
Node采用的方法,是将错误对象作为第一个参数,传入回调函数。这样就避免了捕获代码与发生错误的代码不在同一个时间段的问题。
fs.readFile('/foo.txt', function(err, data) {
if (err !== null) throw err;
console.log(data);
});
上面代码表示,读取文件foo.txt是一个异步操作,它的回调函数有两个参数,第一个是错误对象,第二个是读取到的文件数据。如果第一个参数不是null,就意味着发生错误,后面代码也就不再执行了
EventEmitter接口的error事件
发生错误的时候,也可以用EventEmitter接口抛出error事件。
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.emit('error', new Error('something bad happened'));
使用上面的代码必须小心,因为如果没有对error事件部署监听函数,会导致整个应用程序崩溃。所以,一般总是必须同时部署下面的代码。
emitter.on('error', function(err) {
console.error('出错:' + err.message);
});
详细教程敬请期待更新