1. 开始使用nodejs
1.1. Hello Word
好了,让我们开始实现第一个 Node.js 程序吧。打开你常用的文本编辑器,在其中输入:
console.log('Hello World');
将文件保存为 helloworld.js,打开命令行工具,进入 helloworld.js所在的目录,执行以下命令:
node helloworld.js
如果一切正常,你将会在命令行工具中看到输出 Hello World。
1.2. Node.js命令行工具
在前面的 Hello World 示例中,我们用到了命令行中的 node 命令,输入 node --help
可以看到详细的帮助信息:
Usage: node [options] [ -e script | script.js ] [arguments]
node debug script.js [arguments]
Options:
-v, --version print node's version
-e, --eval script evaluate script
-p, --print print result of --eval
--v8-options print v8 command line options
--vars print various compiled-in variables
--max-stack-size=val set max v8 stack size (bytes)
Environment variables:
NODE_PATH ';'-separated list of directories
prefixed to the module search path.
NODE_MODULE_CONTEXTS Set to 1 to load modules in their own
global contexts.
NODE_DISABLE_COLORS Set to 1 to disable colors in the REPL
Documentation can be found at http://nodejs.org/
其中显示了 node 的用法,运行 Node.js 程序的基本方法就是执行 node script.js,
其中 script.js是脚本的文件名。
除了直接运行脚本文件外, node --help 显示的使用方法中说明了另一种输出 Hello
World 的方式:
$ node -e "console.log('Hello World');"
Hello World
我们可以把要执行的语句作为 node -e 的参数直接执行。
1.3 建立 HTTP 服务器
让我们创建一个 HTTP 服务器吧。建立一个名为 app.js 的文件,内容
为:
//app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
}).listen(3000);
console.log("HTTP server is listening at port 3000.");
小技巧——使用 supervisor
supervisor会监视你对代码的改动,并自动重启 Node.js。
使用方法很简单,首先使用 npm 安装 supervisor:
$ npm install -g supervisor
接下来,使用 supervisor 命令启动 app.js:
$ supervisor app.js
DEBUG: Running node-supervisor with
DEBUG: program 'app.js'
DEBUG: --watch '.'
DEBUG: --extensions 'node|js'
DEBUG: --exec 'node'
DEBUG: Starting child process with 'node app.js'
DEBUG: Watching directory '/home/byvoid/.' for changes.
HTTP server is listening at port 3000.
当代码被改动时,运行的脚本会被终止,然后重新启动。在终端中显示的结果如下:
DEBUG: crashing child
DEBUG: Starting child process with 'node app.js'
HTTP server is listening at port 3000.
2. 异步式编程
2.1. 回调函数
让我们看看在 Node.js 中如何用异步的方式读取一个文件,下面是一个例子:
//readfile.js
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
console.log('end.');
运行结果:
end.
Contents of the file.
Node.js 也提供了同步读取文件的 API:
//readfilesync.js
var fs = require('fs');
var data = fs.readFileSync('file.txt', 'utf-8');
console.log(data);
console.log('end.');
运行的结果与前面不同,如下所示:
$ node readfilesync.js
Contents of the file.
end.
同步式读取文件的方式比较容易理解,将文件名作为参数传入 fs.readFileSync 函数,阻塞等待读取完成后,将文件的内容作为函数的返回值赋给 data变量,接下来控制台输出 data 的值,最后输出 end.。
异步式读取文件就稍微有些违反直觉了, end.先被输出。要想理解结果,我们必须先知道在 Node.js 中,异步式 I/O是通过回调函数来实现的。 fs.readFile 接收了三个参数,第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。JavaScript 支 持 匿 名 的 函 数 定 义 方 式 , 譬 如 我 们 例 子 中 回 调 函 数 的 定 义 就 是 嵌 套 在fs.readFile 的参数表中的。这种定义方式在 JavaScript中极为普遍,与下面这种定义
方式实现的功能是一致的:
//readfilecallback.js
function readFileCallBack(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
}
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', readFileCallBack);
console.log('end.');
2.2. 事件
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。在开发者看来,事件由 EventEmitter 对象提供。前面提到的 fs.readFile 和 http.createServer 的回调函数都是通过 EventEmitter 来实现的。下面我们用一个简单的例子说明 EventEmitter的用法:
//event.js
var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function() {
console.log('some_event occured.');
});
setTimeout(function() {
event.emit('some_event');
}, 1000);
运行这段代码, 1秒后控制台输出了 some_event occured.。其原理是 event 对象注册了事件 some_event 的一个监听器,然后我们通过 setTimeout在1000毫秒以后向
event 对象发送事件 some_event,此时会调用 some_event 的监听器。
3. 模块和包
3.1. 什么是模块
模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、 JSON 或者编译过的 C/C++扩展。
在前面章节的例子中,我们曾经用到了 var http = require('http'), 其中 http是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过require 函数获取了这个模块,然后才能使用其中的对象。
3.2. 创建及加载模块
- 创建模块
在 Node.js 中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问题仅仅在于如何在其他文件中获取这个模块。 Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口, require用于从外部获取一个模块的接口,即所获取模块的 exports 对象。
创建一个 module.js 的文件,内容是:
//module.js
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};
在同一目录下创建 getmodule.js,内容是:
//getmodule.js
var myModule = require('./module');
myModule.setName('BYVoid');
myModule.sayHello();
运行node getmodule.js,结果是:
Hello BYVoid
在以上示例中, module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访问接口,在 getmodule.js 中通过 require('./module') 加载这个模块,然后就可以直接访问 module.js 中 exports 对象的成员函数了。
- 单次加载
上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为require 不会重复加载模块,也就是说无论调用多少次 require, 获得的模块都是同一个。我们在 getmodule.js 的基础上稍作修改:
//loadmodule.js
var hello1 = require('./module');
hello1.setName('BYVoid');
var hello2 = require('./module');
hello2.setName('BYVoid 2');
hello1.sayHello();
运行后发现输出结果是 Hello BYVoid 2,这是因为变量 hello1 和 hello2 指向的是同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。
- 覆盖 exports
有时候我们只是想把一个对象封装到模块中,例如:
//singleobject.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
exports.Hello = Hello;
此时我们在其他文件中需要通过 require('./singleobject').Hello 来获取Hello 对象,这略显冗余,可以用下面方法稍微简化:
//hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;
这样就可以直接获得这个对象了:
//gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();
不可以通过对 exports 直接赋值代替对 module.exports 赋值。exports 实际上只是一个和 module.exports 指向同一个对象的变量,它本身会在模块执行结束后释放,但 module 会,因此只能通过指定module.exports 来改变访问接口。
3.3 创建包
包是在模块基础上更深一步的抽象, Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。 Node.js 根据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。
Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符合 CommonJS 规范的包应该具备以下特征:
- package.json 必须在包的顶层目录下;
- 二进制文件应该在 bin 目录下;
- JavaScript 代码应该在 lib 目录下;
- 文档应该在 doc 目录下;
- 单元测试应该在 test 目录下。
- 作为文件夹的模块
模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一个文件夹。最简单的包,就是一个作为文件夹的模块。下面我们来看一个例子,建立一个叫做 somepackage 的文件夹,在其中创建 index.js,内容如下:
//somepackage/index.js
exports.hello = function() {
console.log('Hello.');
};
然后在 somepackage 之外建立 getpackage.js,内容如下:
//getpackage.js
var somePackage = require('./somepackage');
somePackage.hello();
运行 node getpackage.js,控制台将输出结果 Hello.。
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。
- package.json
在前面例子中的 somepackage 文件夹下,我们创建一个叫做 package.json的文件,内容如下所示:.
{
"main" : "./lib/interface.js"
}
然后将 index.js 重命名为 interface.js 并放入 lib子文件夹下。以同样的方式再次调用这个包,依然可以正常使用。
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找index.js 或 index.node 作为包的接口。
package.json 是 CommonJS 规定的用来描述包的文件,完全符合规范的 package.json 文件应该含有以下字段。
- name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
- description:包的简要说明。
- version:符合语义化版本识别规范的版本字符串。
- keywords:关键字数组,通常用于搜索。
- maintainers:维护者数组,每个元素要包含 name、 email (可选)、 web (可选)字段。
- contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一个元素
- bugs:提交bug的地址,可以是网址或者电子邮件地址。
- licenses:许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到许可证文本的地址)字段。
- repositories:仓库托管地址数组,每个元素要包含 type(仓库的类型,如 git )、url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
- dependencies:包的依赖,一个关联数组,由包名称和版本号组成。下面是一个完全符合 CommonJS 规范的 package.json 示例:
{
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}
3.4 Node.js 包管理器
Node.js包管理器,即npm是 Node.js 官方提供的包管理工具①,它已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。 npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。
4 调试
4.1. 命令行调试
Node.js 支持命令行下的单步调试。下面是一个简单的程序:
var a = 1;
var b = 'world';
var c = function(x) {
console.log('hello ' + x + a);
};
c(b);
在命令行下执行 node debug debug.js,将会启动调试工具:
< debugger listening on port 5858
connecting... ok
break in /home/byvoid/debug.js:1
1 var a = 1;
2 var b = 'world';
3 var c = function(x) {
debug>
命令 | 功能 |
---|---|
run | 执行脚本,在第一行暂停 |
restart | 重新执行脚本 |
cont, c | 继续执行,直到遇到下一个断点 |
next, n | 单步执行 |
step, s | 单步执行并进入函数 |
out, o | 从函数中步出 |
setBreakpoint(), sb() | 在当前行设置断点 |
setBreakpoint(‘f()’), sb(...) | 在函数f的第一行设置断点 |
setBreakpoint(‘script.js’, 20), sb(...) | 在 script.js 的第20行设置断点 |
clearBreakpoint, cb(...) | 清除所有断点 |
backtrace, bt | 显示当前的调用栈 |
list(5) | 显示当前执行到的前后5行代码 |
watch(expr) | 把表达式 expr 加入监视列表 |
unwatch(expr) | 把表达式 expr 从监视列表移除 |
watchers | 显示监视列表中所有的表达式和值 |
repl | 在当前上下文打开即时求值环境 |
kill | 终止当前执行的脚本 |
scripts | 显示当前已加载的所有脚本 |
version | 显示 V8 的版本 |