Node 基礎(chǔ)
JavaScript 是編程語(yǔ)言,而 Node.js 是執(zhí)行環(huán)境。
Node.js 是一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行環(huán)境(runtime)。
Node.js 特性:事件驅(qū)動(dòng)、異步 API、非阻塞 I/O。
Node.js 是專為數(shù)據(jù)密集型實(shí)時(shí)程序(DITR)設(shè)計(jì)的。
Node.js 通過(guò)事件輪詢(event loop)來(lái)實(shí)現(xiàn)非阻塞I/O網(wǎng)絡(luò)的調(diào)用,而事件輪詢是單向運(yùn)行的先入先出隊(duì)列。
ES2015
ECMAScript 2015 是 ECMAScript 標(biāo)準(zhǔn)的第6個(gè)版本,所以有時(shí)候也被稱為 ES6,一般簡(jiǎn)寫(xiě)為 ES2015。
類
ES5 之前需要用 prototype 對(duì)象來(lái)創(chuàng)建類似于類的結(jié)構(gòu):
function User() {
// 構(gòu)造器
}
User.prototype.method = function () {
// 方法
}
ES6 版本支持類:
class User {
constructor() {}
method() {}
}
const 和 let 解決作用域問(wèn)題
在 ES5 中,所有的變量都是用 var 創(chuàng)建的。
應(yīng)該用
const還是let?在決定是用
const還是用let時(shí),幾乎都可以用const。因?yàn)槟愕拇蟛糠执a都是在用你自己的類實(shí)例、對(duì)象常量或不會(huì)變的值,所以大部分情況下都可以用const。即便是有可修改屬性的對(duì)象,也是可以用const聲明的,因?yàn)?const的意思是引用是只讀的,而不是值是不可變的。
原生的 promise 和生成器支持
生成器能把異步 I/O 變成同步編程風(fēng)格。
模版字符串
用反引號(hào)(`)定義模版字符串
// ES5 中,字符串常量不支持插值,也不支持跨行。
// 舊的方法
var a = 1;
console.log('一共有 ' + a + ' 個(gè)雞蛋!');
// 模版字符串語(yǔ)法
var a = 1;
console.log(`一共有 ${a} 個(gè)雞蛋!`);
箭頭函數(shù)
舊版語(yǔ)法:
const http = require('http');
const port = 8080;
const server = http.createServer(function (req, res) {
res.end('Hello, world.');
});
server.listen(port, function () {
console.log('Server listening on: http://localhost:%s', port);
});
箭頭函數(shù)語(yǔ)法:
const http = require('http');
const port = 8080;
const server = http.createServer((req, res) => {
res.end('Hello, world.');
});
server.listen(port, () => {
console.log('Server listening on: http://localhost:%s', port);
});
Node.js 架構(gòu)


說(shuō)明:
- libuv 是提供快速、跨平臺(tái)、非阻塞 I/O 的本地庫(kù);
- V8 負(fù)責(zé) JavaScript 代碼的解釋和執(zhí)行,它可以將 JavaScript 直接編譯為機(jī)器碼;
- C++ 綁定層可以將 libuv 和 V8 結(jié)合起來(lái)。
Node 的使用場(chǎng)景
Node 程序主要可以分成三種類型:Web 應(yīng)用程序、命令行工具、和后臺(tái)程序、桌面程序。


Node 功能的組織及重用
Node 模塊打包代碼是為了重用,但它們不會(huì)改變?nèi)肿饔糜颉?/p>
Node 的模塊系統(tǒng)避免了對(duì)全局作用域的污染,從而也就避免了命名沖突,并簡(jiǎn)化了代碼的重用。
模塊即可以是一個(gè)文件,也可以是包含一個(gè)或多個(gè)文件的目錄(默認(rèn)模塊入口為 index.js 文件)。
currency.js
自定義模塊,在該文件中添加兩個(gè)貨幣轉(zhuǎn)換函數(shù):
方法:通過(guò)設(shè)定 exports 對(duì)象的屬性來(lái)指明要暴露的函數(shù)或變量。
// 私有變量,外界無(wú)法訪問(wèn)到。
var candaianDollar = 0.91;
function roundTwoDecimals(amount) {
return Math.round(amount * 100) / 100;
}
// canadianToUS() 函數(shù)設(shè)定在 exports 模塊中,所以引入這個(gè)模塊的代碼可以使用它。
exports.canadianToUS = function (canadian) {
return roundTwoDecimals(canadian * candaianDollar);
}
// USToCanadian() 函數(shù)也設(shè)定在 exports 模塊中。
exports.USToCanadian = function (us) {
return roundTwoDecimals(us / candaianDollar);
}
test-currency.js
引入模塊
// require() 函數(shù)是一個(gè)同步I/O函數(shù),一般在文件頂端引入。
// 相對(duì)路徑 ./ 表示當(dāng)前同一目錄下。
const currency = require('./currency');
// 使用 currency 模塊的 canadianToUS() 函數(shù)
console.log(currency.canadianToUS(50));
// 使用 currency 模塊的 USToCanadian() 函數(shù)
console.log(currency.USToCanadian(30));
用 module.exports 微調(diào)模塊的創(chuàng)建
如果只需要從模塊中得到一個(gè)函數(shù),那么從 require 中返回一個(gè)函數(shù)的代碼比返回一個(gè)對(duì)象的代碼更優(yōu)雅。
-
不能用任何其他對(duì)象、函數(shù)或者變量給 exports 賦值。
我的理解:可以把函數(shù)或者對(duì)象設(shè)置為 exports 的屬性(
exports.function = {...}),但是不能把函數(shù)或者對(duì)象賦值(exports = function)給 exports。 用 module.exports 可以對(duì)外提供單個(gè)變量、函數(shù)或者對(duì)象。
如果你創(chuàng)建了一個(gè)既有 exports 又有 module.exports 的模塊,那它會(huì)返回 module.exports,而 exports 會(huì)被忽略。
exports 與 module.exports
最終在程序里導(dǎo)出的是module.exports。exports只是對(duì)module.exports的一個(gè)全局引用,最初被定義為一個(gè)可以添加屬性的空對(duì)象。所以exports.myFunc只是module.exports.myFunc的簡(jiǎn)寫(xiě)。
所以,如果把exports設(shè)定為別的,就打破了module.exports和exports之間的引用關(guān)系。
- 根據(jù)需要使用 exports 或 module.exports 可以將功能組織成模塊,規(guī)避掉程序腳本一直增長(zhǎng)產(chǎn)生的弊端。
用 node_modules 重用模塊

- 用環(huán)境變量 NODE_PATH 可以改變 Node 模塊的默認(rèn)路徑。
注意事項(xiàng)
- 如果模塊是目錄,在模塊目錄中定義模塊的文件必須被命名為 index.js,除非你在這個(gè)目錄下一個(gè)叫package.json 的文件里特別指明。
- Node 能把模塊作為對(duì)象緩存起來(lái)。Node 加載模塊的順序:緩存模塊>核心模塊>當(dāng)前目錄模塊>node_modules模塊。
- 有些常用的 Node 核心模塊在 Node 初始化時(shí)就被加載緩存起來(lái)了,所以加載速度相對(duì)也會(huì)更快。
異步編程技術(shù)
Node 中兩種響應(yīng)邏輯管理方式:回調(diào)和事件監(jiān)聽(tīng)。
回調(diào):適用于一次性異步邏輯。
事件發(fā)射器:把異步邏輯跟一個(gè)概念實(shí)體關(guān)聯(lián)起來(lái),可以通過(guò)監(jiān)聽(tīng)器輕松管理。
流程控制:可以管理異步任務(wù)的執(zhí)行順序,串行執(zhí)行或者并行執(zhí)行。
1. 用回調(diào)處理一次性事件
回調(diào)是一個(gè)函數(shù),它被當(dāng)做參數(shù)傳給異步函數(shù),它描述了異步操作完成之后要做什么。

title.json
JSON 文件會(huì)被格式化成一個(gè)包含文章標(biāo)題的字符串?dāng)?shù)組。
[
"Kazakhstan is a huge country...what goes on there?",
"This weather is making me craaazy",
"My neighbor sort of howls at night"
]
template.html
HTML 模版文件,% 會(huì)被替換為博客文章的標(biāo)題。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Latest Posts</h1>
<ul><li>%</li></ul>
</html>
blog_recent.js
功能如下:
- 異步獲取存放在 JSON 文件中的文章的標(biāo)題;
- 異步獲取簡(jiǎn)單的 HTML 模版;
- 把標(biāo)題組裝在 HTML 模版中;
- 把 HTML 頁(yè)面發(fā)送給用戶。
// 獲取 JSON 文件中的標(biāo)題,并渲染 Web 頁(yè)面
'use strict';
const http = require('http');
const fs = require('fs');
const path = require('path');
// 創(chuàng)建 HTTP 服務(wù)器,并用回調(diào)定義響應(yīng)邏輯
http.createServer(function (req, res) {
if (req.url == '/') {
// 讀取 JSON 文件并用回調(diào)定義如何處理其中的內(nèi)容
// fs.readFile() 直接讀取文件的相對(duì)路徑會(huì)報(bào)錯(cuò),這里用 path 拼接。
fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
// 如果出錯(cuò),輸入錯(cuò)誤日志,并給客戶端返回錯(cuò)誤
if (err) {
console.error(err);
res.end('Server Error: Read JSON File Error');
}else {
// 從 JSON 文本中解析數(shù)據(jù)
var titles = JSON.parse(data.toString());
// 讀取 HTML 模版,并在加載完成后使用回調(diào)
fs.readFile(path.join(__dirname, './template.html'), function (err, data) {
if (err) {
console.error(err);
res.end('Server Error: Read html Error');
}else {
var tmp1 = data.toString();
// 組裝 HTML 頁(yè)面以顯示博客標(biāo)題
var html = tmp1.replace('%', titles.join('</li><li>'));
res.writeHead(200, {'Content-Type': 'text/html'});
// 將 HTML 頁(yè)面發(fā)送給用戶
res.end(html);
}
});
}
});
}
}).listen(8000, '127.0.0.1');
這個(gè)示例嵌入了三層回調(diào):
http.createServer(function (req, res) {
fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
fs.readFile(path.join(__dirname, './template.html'), function (err, data) {
});
});
});
優(yōu)化方式:創(chuàng)建中間函數(shù)以減少嵌套(也可以理解為:將代碼模塊化)。
就是把步驟中的單一功能抽象為單獨(dú)的中間函數(shù)。
...
優(yōu)化方式:通過(guò)盡早返回減少嵌套。
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer(function (req, res) {
getTitles(res);
}).listen(8000, "127.0.0.1");
function getTitles(res) {
fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
// 不再創(chuàng)建 else 分支,而是直接return,因?yàn)槿绻鲥e(cuò)的話,也沒(méi)有必要繼續(xù)執(zhí)行這個(gè)函數(shù)了。
if (err) return hadError(err, res);
getTemplate(JSON.parse(data.toString()), res);
});
}
function getTemplate(titles, res) {
fs.readFile(path.join(__dirname, './template.html'), function (err, data) {
if (err) return hadError(err, res);
formatHtml(titles, data.toString(), res);
});
}
function formatHtml(titles, tmp1, res) {
var html = tmp1.replace('%', titles.join('</li><li>'));
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(html);
}
function hadError(err, res) {
console.error(err);
res.end('Server Error');
}
Node 的異步回調(diào)慣例
Node 中的大多數(shù)內(nèi)置模塊在使用回調(diào)時(shí)都會(huì)帶兩個(gè)參數(shù):第一個(gè)用來(lái)放可能會(huì)發(fā)生的錯(cuò)誤,第二個(gè)放結(jié)果。錯(cuò)誤參數(shù)經(jīng)常被縮寫(xiě)為 err。
下面是這個(gè)常用的函數(shù)簽名的典型示例:
var fs = require('fs'); fs.readFile('./titles.json', function (err, data) { if (err) throw err; // do something with data if no error has occurred })
2. 用事件發(fā)射器處理重復(fù)性事件
事件發(fā)射器會(huì)觸發(fā)事件,并且在那些事件被觸發(fā)時(shí)能處理它們。
事件是通過(guò)監(jiān)聽(tīng)器進(jìn)行處理的。
監(jiān)聽(tīng)器是跟事件相關(guān)聯(lián)的、當(dāng)有事件出現(xiàn)時(shí)就會(huì)被觸發(fā)的回調(diào)函數(shù)。
const net = require('net');
var server = net.createServer(function (socket) {
// 當(dāng)有客戶端連接上來(lái)時(shí),它就會(huì)創(chuàng)建一個(gè)socket。
// 用 on 方法添加監(jiān)聽(tīng)器響應(yīng) data 事件。
socket.on('data', function (data) {
socket.write(data);
});
// once 方法,data 事件只是在第一次會(huì)被處理
socket.once('data', function (data) {
socket.write(data);
})
}).listen(8888);
用 Node 內(nèi)置的事件模版創(chuàng)建自己的事件發(fā)射器:
// 定義一個(gè)channel事件發(fā)射器,帶有一個(gè)監(jiān)聽(tīng)器,可以向加入頻道的人做出響應(yīng)。
var EventEmitter = require('events').EventEmitter;
var channel = new EventEmitter();
// 用on(或者用比較長(zhǎng)的addListener)方法給事件發(fā)射器添加了監(jiān)聽(tīng)器:
channel.on('join', function () {
console.log('Welcome!');
})
// 用emit函數(shù)發(fā)射這個(gè)事件
channel.emit('join');
錯(cuò)誤處理
創(chuàng)建發(fā)出 error 類型事件的事件發(fā)射器,而不是直接拋出錯(cuò)誤。
// 創(chuàng)建一個(gè)錯(cuò)誤監(jiān)聽(tīng)器,將被發(fā)出的錯(cuò)誤輸出到控制臺(tái)中:
const events = require('events');
var myEmitter = new events.EventEmitter();
myEmitter.on('error', function (err) {
console.log('ERROR: ' + err.message);
});
myEmitter.emit('error', new Error('Something is wrong.'));
異步開(kāi)發(fā)的難題
創(chuàng)建異步程序時(shí),必須密切關(guān)注程序的執(zhí)行流程、程序的執(zhí)行狀態(tài)...
// 示例:作用域是如何導(dǎo)致 bug 出現(xiàn)的:
function asyncFunction(callback) {
// 200ms 后,執(zhí)行回調(diào)函數(shù)
setTimeout(callback, 200);
}
let color = 'blue';
asyncFunction(() => {
console.log(`The color is ${color}`);
});
color = 'green';
// 輸出結(jié)果:
// [Running] node "/Users/andy/Desktop/node/test_async.js"
// The color is green
用匿名函數(shù)保留全局變量的值:
// JavaScript 編程技巧:用閉包控制程序的狀態(tài)
function asyncFunction(callbback) {
setTimeout(callbback, 200);
}
var conlor = 'blue';
// 將 color 的值傳給匿名函數(shù)
// color 變成了匿名函數(shù)的參數(shù),也就是這個(gè)匿名函數(shù)內(nèi)部的本地變量,
// 當(dāng)匿名函數(shù)外面的color值發(fā)生變化時(shí),本地版的color不會(huì)受影響。
(function(color) {
asyncFunction(function() {
console.log('The color is' + color);
})
})(color);
color = 'green';
異步邏輯的順序化
流程控制:讓一組異步任務(wù)按照順序執(zhí)行。串行/并行。
需要一個(gè)接著一個(gè)做的任務(wù)叫做串行任務(wù)。
不需要一個(gè)接著一個(gè)做的任務(wù)叫做并行任務(wù)。

實(shí)現(xiàn)串行化流程控制
使用回調(diào)讓幾個(gè)異步任務(wù)順序執(zhí)行:
setTimeout(function() {
console.log('1');
setTimeout(function() {
console.log('2');
setTimeout(function() {
console.log('3');
}, 100); // 任務(wù)3,花費(fèi) 0.1 秒
}, 500); // 任務(wù)2,花費(fèi) 0.5 秒
}, 1000); // 任務(wù)1,花費(fèi) 1 秒
第三方模塊:Nimble:這個(gè)模塊官網(wǎng)上顯示7年沒(méi)更新了??????,而且現(xiàn)在流行用 Promise 或者 async 來(lái)實(shí)現(xiàn)。
// 《Node.js 實(shí)戰(zhàn)(第二版)》Async 示例:
const async = require('async');
// 給 Async 一個(gè)函數(shù)數(shù)組,讓它一個(gè)接一個(gè)地執(zhí)行
async.series([
callback => {
setTimeout(() => {
console.log('I execute first.');
callback();
}, 1000);
},
callback => {
setTimeout(() => {
console.log('I execute next.');
callback();
}, 500);
},
callback => {
setTimeout(() => {
console.log('I execute last.');
callback();
}, 100);
},
]);
// 執(zhí)行結(jié)果:
// [Running] node "/Users/andy/Desktop/node/test_async.js"
// I execute first.
// I execute next.
// I execute last.
串行化流程控制的工作機(jī)制:
為了用串行化流程控制讓幾個(gè)異步任務(wù)按順序執(zhí)行,需要先把這些任務(wù)按預(yù)期的執(zhí)行順序放到一個(gè)數(shù)組中。
這個(gè)數(shù)組將起到隊(duì)列的作用:完成一個(gè)任務(wù)后按順序從數(shù)組中取出下一個(gè)。
Demo示例:實(shí)現(xiàn)串行化流程控制。
實(shí)現(xiàn)并行化流程控制
為了讓異步任務(wù)并行執(zhí)行,仍然是要把任務(wù)放到數(shù)組中,但任務(wù)的存放順序無(wú)關(guān)緊要。每個(gè)任務(wù)都應(yīng)該調(diào)用處理器函數(shù)增加已完成任務(wù)的計(jì)數(shù)值。當(dāng)所有任務(wù)都完成后,處理器函數(shù)應(yīng)該執(zhí)行后續(xù)的邏輯。
'use strict';
const async = require('async');
const exec = require('child_process').exec;
// 輔助函數(shù):下載指定版本的 Node.js 源碼
function downloadNodeVersion(version, destination, callback) {
const url = `http://nodejs.org/dist/v${version}/node-v${version}.tar.gz`;
const filepath = `${destination}/${version}.tgz`;
exec(`curl ${url} > ${filepath}`, callback);
}
// 串行執(zhí)行兩個(gè)任務(wù):
// 任務(wù)一:并行下載兩個(gè)版本的源碼;
// 任務(wù)二:將下載好的版本歸檔到一個(gè)新文件中。
// 用串行化流程控制保證在文件下載完成之前不會(huì)做歸檔處理。
async.series([
callback => {
// 并行下載
async.parallel([
callback => {
console.log('Downloading Node V4.4.7...');
downloadNodeVersion('4.4.7', '/tmp', callback);
},
callback => {
console.log('Downloading Node v6.3.0');
downloadNodeVersion('6.3.0', '/tmp', callback);
},
], callback);
},
callback => {
console.log('Creating archive of download files ...');
exec(
'tar cvf node_distros.tar /tmp/4.4.7.tgz /tmp/6.3.0.tgz',
err => {
if (err) throw err;
console.log('All down!');
callback();
}
);
},
], (err, results) => {
if (err) throw err;
console.log(results);
});