1. 開始使用nodejs
1.1. Hello Word
好了,讓我們開始實(shí)現(xiàn)第一個(gè) Node.js 程序吧。打開你常用的文本編輯器,在其中輸入:
console.log('Hello World');
將文件保存為 helloworld.js,打開命令行工具,進(jìn)入 helloworld.js所在的目錄,執(zhí)行以下命令:
node helloworld.js
如果一切正常,你將會在命令行工具中看到輸出 Hello World。
1.2. Node.js命令行工具
在前面的 Hello World 示例中,我們用到了命令行中的 node 命令,輸入 node --help
可以看到詳細(xì)的幫助信息:
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 的用法,運(yùn)行 Node.js 程序的基本方法就是執(zhí)行 node script.js,
其中 script.js是腳本的文件名。
除了直接運(yùn)行腳本文件外, node --help 顯示的使用方法中說明了另一種輸出 Hello
World 的方式:
$ node -e "console.log('Hello World');"
Hello World
我們可以把要執(zhí)行的語句作為 node -e 的參數(shù)直接執(zhí)行。
1.3 建立 HTTP 服務(wù)器
讓我們創(chuàng)建一個(gè) HTTP 服務(wù)器吧。建立一個(gè)名為 app.js 的文件,內(nèi)容
為:
//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會監(jiān)視你對代碼的改動,并自動重啟 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.
當(dāng)代碼被改動時(shí),運(yùn)行的腳本會被終止,然后重新啟動。在終端中顯示的結(jié)果如下:
DEBUG: crashing child
DEBUG: Starting child process with 'node app.js'
HTTP server is listening at port 3000.
2. 異步式編程
2.1. 回調(diào)函數(shù)
讓我們看看在 Node.js 中如何用異步的方式讀取一個(gè)文件,下面是一個(gè)例子:
//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.');
運(yùn)行結(jié)果:
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.');
運(yùn)行的結(jié)果與前面不同,如下所示:
$ node readfilesync.js
Contents of the file.
end.
同步式讀取文件的方式比較容易理解,將文件名作為參數(shù)傳入 fs.readFileSync 函數(shù),阻塞等待讀取完成后,將文件的內(nèi)容作為函數(shù)的返回值賦給 data變量,接下來控制臺輸出 data 的值,最后輸出 end.。
異步式讀取文件就稍微有些違反直覺了, end.先被輸出。要想理解結(jié)果,我們必須先知道在 Node.js 中,異步式 I/O是通過回調(diào)函數(shù)來實(shí)現(xiàn)的。 fs.readFile 接收了三個(gè)參數(shù),第一個(gè)是文件名,第二個(gè)是編碼方式,第三個(gè)是一個(gè)函數(shù),我們稱這個(gè)函數(shù)為回調(diào)函數(shù)。JavaScript 支 持 匿 名 的 函 數(shù) 定 義 方 式 , 譬 如 我 們 例 子 中 回 調(diào) 函 數(shù) 的 定 義 就 是 嵌 套 在fs.readFile 的參數(shù)表中的。這種定義方式在 JavaScript中極為普遍,與下面這種定義
方式實(shí)現(xiàn)的功能是一致的:
//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 操作在完成時(shí)都會發(fā)送一個(gè)事件到事件隊(duì)列。在開發(fā)者看來,事件由 EventEmitter 對象提供。前面提到的 fs.readFile 和 http.createServer 的回調(diào)函數(shù)都是通過 EventEmitter 來實(shí)現(xiàn)的。下面我們用一個(gè)簡單的例子說明 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);
運(yùn)行這段代碼, 1秒后控制臺輸出了 some_event occured.。其原理是 event 對象注冊了事件 some_event 的一個(gè)監(jiān)聽器,然后我們通過 setTimeout在1000毫秒以后向
event 對象發(fā)送事件 some_event,此時(shí)會調(diào)用 some_event 的監(jiān)聽器。
3. 模塊和包
3.1. 什么是模塊
模塊是 Node.js 應(yīng)用程序的基本組成部分,文件和模塊是一一對應(yīng)的。換言之,一個(gè)Node.js 文件就是一個(gè)模塊,這個(gè)文件可能是 JavaScript 代碼、 JSON 或者編譯過的 C/C++擴(kuò)展。
在前面章節(jié)的例子中,我們曾經(jīng)用到了 var http = require('http'), 其中 http是 Node.js 的一個(gè)核心模塊,其內(nèi)部是用 C++ 實(shí)現(xiàn)的,外部用 JavaScript 封裝。我們通過require 函數(shù)獲取了這個(gè)模塊,然后才能使用其中的對象。
3.2. 創(chuàng)建及加載模塊
- 創(chuàng)建模塊
在 Node.js 中,創(chuàng)建一個(gè)模塊非常簡單,因?yàn)橐粋€(gè)文件就是一個(gè)模塊,我們要關(guān)注的問題僅僅在于如何在其他文件中獲取這個(gè)模塊。 Node.js 提供了 exports 和 require 兩個(gè)對象,其中 exports 是模塊公開的接口, require用于從外部獲取一個(gè)模塊的接口,即所獲取模塊的 exports 對象。
創(chuàng)建一個(gè) module.js 的文件,內(nèi)容是:
//module.js
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};
在同一目錄下創(chuàng)建 getmodule.js,內(nèi)容是:
//getmodule.js
var myModule = require('./module');
myModule.setName('BYVoid');
myModule.sayHello();
運(yùn)行node getmodule.js,結(jié)果是:
Hello BYVoid
在以上示例中, module.js 通過 exports 對象把 setName 和 sayHello 作為模塊的訪問接口,在 getmodule.js 中通過 require('./module') 加載這個(gè)模塊,然后就可以直接訪問 module.js 中 exports 對象的成員函數(shù)了。
- 單次加載
上面這個(gè)例子有點(diǎn)類似于創(chuàng)建一個(gè)對象,但實(shí)際上和對象又有本質(zhì)的區(qū)別,因?yàn)閞equire 不會重復(fù)加載模塊,也就是說無論調(diào)用多少次 require, 獲得的模塊都是同一個(gè)。我們在 getmodule.js 的基礎(chǔ)上稍作修改:
//loadmodule.js
var hello1 = require('./module');
hello1.setName('BYVoid');
var hello2 = require('./module');
hello2.setName('BYVoid 2');
hello1.sayHello();
運(yùn)行后發(fā)現(xiàn)輸出結(jié)果是 Hello BYVoid 2,這是因?yàn)樽兞?hello1 和 hello2 指向的是同一個(gè)實(shí)例,因此 hello1.setName 的結(jié)果被 hello2.setName 覆蓋,最終輸出結(jié)果是由后者決定的。
- 覆蓋 exports
有時(shí)候我們只是想把一個(gè)對象封裝到模塊中,例如:
//singleobject.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
exports.Hello = Hello;
此時(shí)我們在其他文件中需要通過 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;
這樣就可以直接獲得這個(gè)對象了:
//gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();
不可以通過對 exports 直接賦值代替對 module.exports 賦值。exports 實(shí)際上只是一個(gè)和 module.exports 指向同一個(gè)對象的變量,它本身會在模塊執(zhí)行結(jié)束后釋放,但 module 會,因此只能通過指定module.exports 來改變訪問接口。
3.3 創(chuàng)建包
包是在模塊基礎(chǔ)上更深一步的抽象, Node.js 的包類似于 C/C++ 的函數(shù)庫或者 Java/.Net的類庫。它將某個(gè)獨(dú)立的功能封裝起來,用于發(fā)布、更新、依賴管理和版本控制。 Node.js 根據(jù) CommonJS 規(guī)范實(shí)現(xiàn)了包機(jī)制,開發(fā)了 npm來解決包的發(fā)布和獲取需求。
Node.js 的包是一個(gè)目錄,其中包含一個(gè) JSON 格式的包說明文件 package.json。嚴(yán)格符合 CommonJS 規(guī)范的包應(yīng)該具備以下特征:
- package.json 必須在包的頂層目錄下;
- 二進(jìn)制文件應(yīng)該在 bin 目錄下;
- JavaScript 代碼應(yīng)該在 lib 目錄下;
- 文檔應(yīng)該在 doc 目錄下;
- 單元測試應(yīng)該在 test 目錄下。
- 作為文件夾的模塊
模塊與文件是一一對應(yīng)的。文件不僅可以是 JavaScript 代碼或二進(jìn)制代碼,還可以是一個(gè)文件夾。最簡單的包,就是一個(gè)作為文件夾的模塊。下面我們來看一個(gè)例子,建立一個(gè)叫做 somepackage 的文件夾,在其中創(chuàng)建 index.js,內(nèi)容如下:
//somepackage/index.js
exports.hello = function() {
console.log('Hello.');
};
然后在 somepackage 之外建立 getpackage.js,內(nèi)容如下:
//getpackage.js
var somePackage = require('./somepackage');
somePackage.hello();
運(yùn)行 node getpackage.js,控制臺將輸出結(jié)果 Hello.。
我們使用這種方法可以把文件夾封裝為一個(gè)模塊,即所謂的包。包通常是一些模塊的集合,在模塊的基礎(chǔ)上提供了更高層的抽象,相當(dāng)于提供了一些固定接口的函數(shù)庫。通過定制package.json,我們可以創(chuàng)建更復(fù)雜、更完善、更符合規(guī)范的包用于發(fā)布。
- package.json
在前面例子中的 somepackage 文件夾下,我們創(chuàng)建一個(gè)叫做 package.json的文件,內(nèi)容如下所示:.
{
"main" : "./lib/interface.js"
}
然后將 index.js 重命名為 interface.js 并放入 lib子文件夾下。以同樣的方式再次調(diào)用這個(gè)包,依然可以正常使用。
Node.js 在調(diào)用某個(gè)包時(shí),會首先檢查包中 package.json 文件的 main 字段,將其作為包的接口模塊,如果 package.json 或 main 字段不存在,會嘗試尋找index.js 或 index.node 作為包的接口。
package.json 是 CommonJS 規(guī)定的用來描述包的文件,完全符合規(guī)范的 package.json 文件應(yīng)該含有以下字段。
- name:包的名稱,必須是唯一的,由小寫英文字母、數(shù)字和下劃線組成,不能包含空格。
- description:包的簡要說明。
- version:符合語義化版本識別規(guī)范的版本字符串。
- keywords:關(guān)鍵字?jǐn)?shù)組,通常用于搜索。
- maintainers:維護(hù)者數(shù)組,每個(gè)元素要包含 name、 email (可選)、 web (可選)字段。
- contributors:貢獻(xiàn)者數(shù)組,格式與maintainers相同。包的作者應(yīng)該是貢獻(xiàn)者數(shù)組的第一個(gè)元素
- bugs:提交bug的地址,可以是網(wǎng)址或者電子郵件地址。
- licenses:許可證數(shù)組,每個(gè)元素要包含 type (許可證的名稱)和 url (鏈接到許可證文本的地址)字段。
- repositories:倉庫托管地址數(shù)組,每個(gè)元素要包含 type(倉庫的類型,如 git )、url (倉庫的地址)和 path (相對于倉庫的路徑,可選)字段。
- dependencies:包的依賴,一個(gè)關(guān)聯(lián)數(shù)組,由包名稱和版本號組成。下面是一個(gè)完全符合 CommonJS 規(guī)范的 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 官方提供的包管理工具①,它已經(jīng)成了 Node.js 包的標(biāo)準(zhǔn)發(fā)布平臺,用于 Node.js 包的發(fā)布、傳播、依賴控制。 npm 提供了命令行工具,使你可以方便地下載、安裝、升級、刪除包,也可以讓你作為開發(fā)者發(fā)布并維護(hù)包。
4 調(diào)試
4.1. 命令行調(diào)試
Node.js 支持命令行下的單步調(diào)試。下面是一個(gè)簡單的程序:
var a = 1;
var b = 'world';
var c = function(x) {
console.log('hello ' + x + a);
};
c(b);
在命令行下執(zhí)行 node debug debug.js,將會啟動調(diào)試工具:
< 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 | 執(zhí)行腳本,在第一行暫停 |
restart | 重新執(zhí)行腳本 |
cont, c | 繼續(xù)執(zhí)行,直到遇到下一個(gè)斷點(diǎn) |
next, n | 單步執(zhí)行 |
step, s | 單步執(zhí)行并進(jìn)入函數(shù) |
out, o | 從函數(shù)中步出 |
setBreakpoint(), sb() | 在當(dāng)前行設(shè)置斷點(diǎn) |
setBreakpoint(‘f()’), sb(...) | 在函數(shù)f的第一行設(shè)置斷點(diǎn) |
setBreakpoint(‘script.js’, 20), sb(...) | 在 script.js 的第20行設(shè)置斷點(diǎn) |
clearBreakpoint, cb(...) | 清除所有斷點(diǎn) |
backtrace, bt | 顯示當(dāng)前的調(diào)用棧 |
list(5) | 顯示當(dāng)前執(zhí)行到的前后5行代碼 |
watch(expr) | 把表達(dá)式 expr 加入監(jiān)視列表 |
unwatch(expr) | 把表達(dá)式 expr 從監(jiān)視列表移除 |
watchers | 顯示監(jiān)視列表中所有的表達(dá)式和值 |
repl | 在當(dāng)前上下文打開即時(shí)求值環(huán)境 |
kill | 終止當(dāng)前執(zhí)行的腳本 |
scripts | 顯示當(dāng)前已加載的所有腳本 |
version | 顯示 V8 的版本 |