Node 基礎(chǔ)
JavaScript 是編程語言,而 Node.js 是執(zhí)行環(huán)境涮总。
Node.js 是一個基于 Chrome V8 引擎的 JavaScript 運行環(huán)境(runtime)窿祥。
Node.js 特性:事件驅(qū)動解总、異步 API凯亮、非阻塞 I/O较曼。
Node.js 是專為數(shù)據(jù)密集型實時程序(DITR)設(shè)計的糕珊。
Node.js 通過事件輪詢(event loop)來實現(xiàn)非阻塞I/O網(wǎng)絡(luò)的調(diào)用动分,而事件輪詢是單向運行的先入先出隊列。
ES2015
ECMAScript 2015 是 ECMAScript 標(biāo)準(zhǔn)的第6個版本红选,所以有時候也被稱為 ES6澜公,一般簡寫為 ES2015。
類
ES5 之前需要用 prototype 對象來創(chuàng)建類似于類的結(jié)構(gòu):
function User() {
// 構(gòu)造器
}
User.prototype.method = function () {
// 方法
}
ES6 版本支持類:
class User {
constructor() {}
method() {}
}
const 和 let 解決作用域問題
在 ES5 中喇肋,所有的變量都是用 var
創(chuàng)建的坟乾。
應(yīng)該用
const
還是let
?在決定是用
const
還是用let
時蝶防,幾乎都可以用const
甚侣。因為你的大部分代碼都是在用你自己的類實例、對象常量或不會變的值间学,所以大部分情況下都可以用const
殷费。即便是有可修改屬性的對象,也是可以用const
聲明的低葫,因為const
的意思是引用是只讀的详羡,而不是值是不可變的。
原生的 promise 和生成器支持
生成器能把異步 I/O 變成同步編程風(fēng)格嘿悬。
模版字符串
用反引號(`)定義模版字符串
// ES5 中实柠,字符串常量不支持插值,也不支持跨行善涨。
// 舊的方法
var a = 1;
console.log('一共有 ' + a + ' 個雞蛋窒盐!');
// 模版字符串語法
var a = 1;
console.log(`一共有 ${a} 個雞蛋!`);
箭頭函數(shù)
舊版語法:
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ù)語法:
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)
說明:
- libuv 是提供快速钢拧、跨平臺蟹漓、非阻塞 I/O 的本地庫;
- V8 負(fù)責(zé) JavaScript 代碼的解釋和執(zhí)行娶靡,它可以將 JavaScript 直接編譯為機(jī)器碼牧牢;
- C++ 綁定層可以將 libuv 和 V8 結(jié)合起來。
Node 的使用場景
Node 程序主要可以分成三種類型:Web 應(yīng)用程序、命令行工具塔鳍、和后臺程序伯铣、桌面程序。
Node 功能的組織及重用
Node 模塊打包代碼是為了重用轮纫,但它們不會改變?nèi)肿饔糜颉?/p>
Node 的模塊系統(tǒng)避免了對全局作用域的污染腔寡,從而也就避免了命名沖突,并簡化了代碼的重用掌唾。
模塊即可以是一個文件放前,也可以是包含一個或多個文件的目錄(默認(rèn)模塊入口為 index.js 文件)。
currency.js
自定義模塊糯彬,在該文件中添加兩個貨幣轉(zhuǎn)換函數(shù):
方法:通過設(shè)定 exports
對象的屬性來指明要暴露的函數(shù)或變量凭语。
// 私有變量,外界無法訪問到撩扒。
var candaianDollar = 0.91;
function roundTwoDecimals(amount) {
return Math.round(amount * 100) / 100;
}
// canadianToUS() 函數(shù)設(shè)定在 exports 模塊中似扔,所以引入這個模塊的代碼可以使用它。
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ù)是一個同步I/O函數(shù)炒辉,一般在文件頂端引入。
// 相對路徑 ./ 表示當(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)建
如果只需要從模塊中得到一個函數(shù)黔寇,那么從 require 中返回一個函數(shù)的代碼比返回一個對象的代碼更優(yōu)雅。
-
不能用任何其他對象斩萌、函數(shù)或者變量給 exports 賦值缝裤。
我的理解:可以把函數(shù)或者對象設(shè)置為 exports 的屬性(
exports.function = {...}
),但是不能把函數(shù)或者對象賦值(exports = function
)給 exports颊郎。 用 module.exports 可以對外提供單個變量倘是、函數(shù)或者對象。
如果你創(chuàng)建了一個既有 exports 又有 module.exports 的模塊袭艺,那它會返回 module.exports,而 exports 會被忽略叨粘。
exports 與 module.exports
最終在程序里導(dǎo)出的是module.exports猾编。exports只是對module.exports的一個全局引用,最初被定義為一個可以添加屬性的空對象升敲。所以exports.myFunc只是module.exports.myFunc的簡寫答倡。
所以,如果把exports設(shè)定為別的驴党,就打破了module.exports和exports之間的引用關(guān)系瘪撇。
- 根據(jù)需要使用 exports 或 module.exports 可以將功能組織成模塊,規(guī)避掉程序腳本一直增長產(chǎn)生的弊端。
用 node_modules 重用模塊
- 用環(huán)境變量 NODE_PATH 可以改變 Node 模塊的默認(rèn)路徑倔既。
注意事項
- 如果模塊是目錄恕曲,在模塊目錄中定義模塊的文件必須被命名為 index.js,除非你在這個目錄下一個叫package.json 的文件里特別指明渤涌。
- Node 能把模塊作為對象緩存起來佩谣。Node 加載模塊的順序:緩存模塊>核心模塊>當(dāng)前目錄模塊>node_modules模塊。
- 有些常用的 Node 核心模塊在 Node 初始化時就被加載緩存起來了实蓬,所以加載速度相對也會更快茸俭。
異步編程技術(shù)
Node 中兩種響應(yīng)邏輯管理方式:回調(diào)和事件監(jiān)聽。
回調(diào):適用于一次性異步邏輯安皱。
事件發(fā)射器:把異步邏輯跟一個概念實體關(guān)聯(lián)起來调鬓,可以通過監(jiān)聽器輕松管理。
流程控制:可以管理異步任務(wù)的執(zhí)行順序酌伊,串行執(zhí)行或者并行執(zhí)行腾窝。
1. 用回調(diào)處理一次性事件
回調(diào)是一個函數(shù),它被當(dāng)做參數(shù)傳給異步函數(shù)腺晾,它描述了異步操作完成之后要做什么燕锥。
title.json
JSON 文件會被格式化成一個包含文章標(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 模版文件悯蝉,% 會被替換為博客文章的標(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)題;
- 異步獲取簡單的 HTML 模版鼻由;
- 把標(biāo)題組裝在 HTML 模版中暇榴;
- 把 HTML 頁面發(fā)送給用戶。
// 獲取 JSON 文件中的標(biāo)題蕉世,并渲染 Web 頁面
'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() 直接讀取文件的相對路徑會報錯,這里用 path 拼接狠轻。
fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
// 如果出錯奸例,輸入錯誤日志,并給客戶端返回錯誤
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 頁面以顯示博客標(biāo)題
var html = tmp1.replace('%', titles.join('</li><li>'));
res.writeHead(200, {'Content-Type': 'text/html'});
// 將 HTML 頁面發(fā)送給用戶
res.end(html);
}
});
}
});
}
}).listen(8000, '127.0.0.1');
這個示例嵌入了三層回調(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ù)以減少嵌套(也可以理解為:將代碼模塊化)查吊。
就是把步驟中的單一功能抽象為單獨的中間函數(shù)。
...
優(yōu)化方式:通過盡早返回減少嵌套湖蜕。
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,因為如果出錯的話昭抒,也沒有必要繼續(xù)執(zhí)行這個函數(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ù):第一個用來放可能會發(fā)生的錯誤炼杖,第二個放結(jié)果。錯誤參數(shù)經(jīng)常被縮寫為 err盗迟。
下面是這個常用的函數(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ā)射器會觸發(fā)事件坤邪,并且在那些事件被觸發(fā)時能處理它們。
事件是通過監(jiān)聽器進(jìn)行處理的诈乒。
監(jiān)聽器是跟事件相關(guān)聯(lián)的罩扇、當(dāng)有事件出現(xiàn)時就會被觸發(fā)的回調(diào)函數(shù)。
const net = require('net');
var server = net.createServer(function (socket) {
// 當(dāng)有客戶端連接上來時怕磨,它就會創(chuàng)建一個socket喂饥。
// 用 on 方法添加監(jiān)聽器響應(yīng) data 事件。
socket.on('data', function (data) {
socket.write(data);
});
// once 方法肠鲫,data 事件只是在第一次會被處理
socket.once('data', function (data) {
socket.write(data);
})
}).listen(8888);
用 Node 內(nèi)置的事件模版創(chuàng)建自己的事件發(fā)射器:
// 定義一個channel事件發(fā)射器员帮,帶有一個監(jiān)聽器,可以向加入頻道的人做出響應(yīng)导饲。
var EventEmitter = require('events').EventEmitter;
var channel = new EventEmitter();
// 用on(或者用比較長的addListener)方法給事件發(fā)射器添加了監(jiān)聽器:
channel.on('join', function () {
console.log('Welcome!');
})
// 用emit函數(shù)發(fā)射這個事件
channel.emit('join');
錯誤處理
創(chuàng)建發(fā)出 error 類型事件的事件發(fā)射器捞高,而不是直接拋出錯誤。
// 創(chuàng)建一個錯誤監(jiān)聽器渣锦,將被發(fā)出的錯誤輸出到控制臺中:
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.'));
異步開發(fā)的難題
創(chuàng)建異步程序時硝岗,必須密切關(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ù)型檀,也就是這個匿名函數(shù)內(nèi)部的本地變量,
// 當(dāng)匿名函數(shù)外面的color值發(fā)生變化時听盖,本地版的color不會受影響胀溺。
(function(color) {
asyncFunction(function() {
console.log('The color is' + color);
})
})(color);
color = 'green';
異步邏輯的順序化
流程控制:讓一組異步任務(wù)按照順序執(zhí)行。串行/并行皆看。
需要一個接著一個做的任務(wù)叫做串行任務(wù)仓坞。
不需要一個接著一個做的任務(wù)叫做并行任務(wù)。
實現(xiàn)串行化流程控制
使用回調(diào)讓幾個異步任務(wù)順序執(zhí)行:
setTimeout(function() {
console.log('1');
setTimeout(function() {
console.log('2');
setTimeout(function() {
console.log('3');
}, 100); // 任務(wù)3腰吟,花費 0.1 秒
}, 500); // 任務(wù)2无埃,花費 0.5 秒
}, 1000); // 任務(wù)1,花費 1 秒
第三方模塊:Nimble:這個模塊官網(wǎng)上顯示7年沒更新了??????毛雇,而且現(xiàn)在流行用 Promise 或者 async 來實現(xiàn)录语。
// 《Node.js 實戰(zhàn)(第二版)》Async 示例:
const async = require('async');
// 給 Async 一個函數(shù)數(shù)組,讓它一個接一個地執(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ī)制:
為了用串行化流程控制讓幾個異步任務(wù)按順序執(zhí)行禾乘,需要先把這些任務(wù)按預(yù)期的執(zhí)行順序放到一個數(shù)組中。
這個數(shù)組將起到隊列的作用:完成一個任務(wù)后按順序從數(shù)組中取出下一個虽缕。
Demo示例:實現(xiàn)串行化流程控制始藕。
實現(xiàn)并行化流程控制
為了讓異步任務(wù)并行執(zhí)行蒲稳,仍然是要把任務(wù)放到數(shù)組中,但任務(wù)的存放順序無關(guān)緊要伍派。每個任務(wù)都應(yīng)該調(diào)用處理器函數(shù)增加已完成任務(wù)的計數(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í)行兩個任務(wù):
// 任務(wù)一:并行下載兩個版本的源碼祥国;
// 任務(wù)二:將下載好的版本歸檔到一個新文件中。
// 用串行化流程控制保證在文件下載完成之前不會做歸檔處理晾腔。
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);
});