
零、什么是Node.js?
引用Node.js官方網(wǎng)站的解釋如下:
Node.js? is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
翻譯成中文就是:
Node.js 是一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行環(huán)境。
Node.js 使用了一個(gè)事件驅(qū)動(dòng)、非阻塞式 I/O 的模型,使其輕量又高效。
1、 運(yùn)行環(huán)境(Runtime)
如果做一個(gè)類(lèi)比,Node.js與JavaScript關(guān)系,就像JDK(Java Development Kit)與Java的關(guān)系。
總的來(lái)說(shuō),Node.js不是一門(mén)語(yǔ)言,而是用來(lái)進(jìn)行Web開(kāi)發(fā)的Runtime。
2、事件驅(qū)動(dòng)(Event-driven)
在前端web開(kāi)發(fā)中比較常見(jiàn)的事件驅(qū)動(dòng)例子是,給一個(gè)按鈕綁定一個(gè)事件處理程序,這個(gè)事件處理程序就是事件驅(qū)動(dòng)的,JavaScript進(jìn)程并不知道什么時(shí)候調(diào)用它,點(diǎn)擊按鈕,觸發(fā)Click事件,此時(shí)主程序得到相應(yīng)的通知,就知道調(diào)用綁定的的事件處理程序了。
因?yàn)镹ode.js是JavaScript的Runtime,所以天然就可以使用這種模式通知主進(jìn)程的I/O 完成。
3、非阻塞式 I/O(Non-blocking I/O)
阻塞:I/O 時(shí)進(jìn)程休眠等待 I/O 完成后進(jìn)行下一步
非阻塞:I/O 時(shí)函數(shù)立即返回,進(jìn)程不等待I/O 完成
一、Node.js 究竟好在哪里?
1、為什么偏愛(ài)Node.js
① 前端需求變得重要、職責(zé)范圍變大,統(tǒng)一開(kāi)發(fā)體驗(yàn)
② 在處理高并發(fā)、I/O 密集型場(chǎng)景性能優(yōu)勢(shì)明顯
Node.js 使用了事件驅(qū)動(dòng)和非阻塞的 I/O 模型,使 Node 輕量高效,非常適合 I/O 密集的 Web 場(chǎng)景。
CPU密集型 VS I/O密集型
CPU密集型:計(jì)算等邏輯判斷的操作,如:壓縮、解壓、加密和解密等。
I/O 密集型:存取設(shè)備,網(wǎng)絡(luò)設(shè)施的讀取操作,如:文件的存取,http等網(wǎng)絡(luò)操作,數(shù)據(jù)庫(kù)操作等。
2、Web常見(jiàn)場(chǎng)景
① 靜態(tài)資源讀取
html,css,js等文件的讀取
② 數(shù)據(jù)庫(kù)操作
把數(shù)據(jù)存取到物理設(shè)磁盤(pán)或內(nèi)存中
③ 渲染頁(yè)面
讀取模板文件,根據(jù)數(shù)據(jù)生成html
3、高并發(fā)應(yīng)對(duì)之道
高并發(fā),簡(jiǎn)而言之就是單位時(shí)間內(nèi)訪問(wèn)量特別大。
對(duì)應(yīng)生活中的場(chǎng)景,一家菜館做菜招待顧客,老板剛開(kāi)始就雇了一個(gè)廚師,做菜好吃不貴,顧客很多,顧客排好一條隊(duì),然后顧客選好菜,廚師拿到菜單開(kāi)始做菜,做好菜,給顧客端上來(lái),再招待下個(gè)顧客。

客人增多,一個(gè)廚師忙不過(guò)來(lái)了,老板于是又招了2個(gè)廚師,這樣顧客可以排3條隊(duì),快了很多。

隨著菜館名氣增大,顧客越來(lái)越多,老板本想再用之前的方法多招幾個(gè)廚師,但是老板想,多招廚師好像不太劃算,有些廚師做飯快,有些做飯慢,經(jīng)過(guò)調(diào)查,老板發(fā)現(xiàn)做飯快2倍的廚師只需要花費(fèi)原來(lái)廚師工資的1.5倍,于是精明的老板炒掉了原來(lái)的3個(gè)廚師,招來(lái)了比原來(lái)廚師做飯速度快2倍的另外3個(gè)廚師,菜館比之前運(yùn)轉(zhuǎn)的更好了。

回到Web開(kāi)發(fā)場(chǎng)景,廚師就是物理服務(wù)器,應(yīng)對(duì)高并發(fā)的方法如下:① 增加機(jī)器數(shù)
機(jī)器多了,流量還是一樣的大,通過(guò)Nginx負(fù)載均衡分到不同的機(jī)器上處理
② 增加每臺(tái)機(jī)器的CPU數(shù)——多核
單位機(jī)器,核數(shù)增多,運(yùn)算能力增強(qiáng)了
4、進(jìn)程與線程
進(jìn)程在百度百科中的解釋如下:
進(jìn)程(Process)是計(jì)算機(jī)中的程序關(guān)于某數(shù)據(jù)集合上的一次運(yùn)行活動(dòng),是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。
換成正常的人話就是:電腦桌面的程序,如QQ音樂(lè),當(dāng)我們雙擊圖標(biāo)時(shí),實(shí)際上是把這個(gè)程序加載到內(nèi)存中執(zhí)行,我們稱(chēng)這個(gè)執(zhí)行中的程序就是進(jìn)程,操作系統(tǒng)都是用進(jìn)程作為基本單位進(jìn)行分配和調(diào)度的。
多進(jìn)程:?jiǎn)?dòng)多個(gè)進(jìn)程,多個(gè)進(jìn)程可以一塊執(zhí)行多個(gè)任務(wù)。
線程,進(jìn)程內(nèi)一個(gè)相對(duì)獨(dú)立的、可調(diào)度的執(zhí)行單元,與同屬一個(gè)進(jìn)程的線程共享進(jìn)程的資源。
多線程,啟動(dòng)一個(gè)進(jìn)程,在一個(gè)進(jìn)程內(nèi)啟動(dòng)多個(gè)線程,這樣,多個(gè)線程也可以一塊執(zhí)行多個(gè)任務(wù)。
5、Node.js工作模型
傳統(tǒng)的server處理請(qǐng)求(如多線程高并發(fā)模式的Apache)對(duì)應(yīng)生活中的場(chǎng)景,如下:
一個(gè)老板開(kāi)了一家飯店,不同于之前那個(gè)菜館,這家的每個(gè)廚師配備了一個(gè)服務(wù)員,專(zhuān)門(mén)負(fù)責(zé)點(diǎn)菜,然后把菜單給廚師,廚師負(fù)責(zé)做菜,做完后給服務(wù)員,服務(wù)員端給客人,然后再接待顧客隊(duì)伍中的下一個(gè)。
如果這個(gè)飯店是Web的話,點(diǎn)菜這個(gè)動(dòng)作很快,相當(dāng)于CPU的運(yùn)算,如訪問(wèn)一個(gè)靜態(tài)資源,CPU運(yùn)算后知道是哪個(gè)文件了,去相應(yīng)盤(pán)讀取,類(lèi)似于廚師做飯,是一個(gè)相對(duì)較慢的阻塞I/O操作,當(dāng)顧客很多時(shí)候就相當(dāng)于高并發(fā)了。忙不過(guò)來(lái)的時(shí)候,可以選擇增加廚師數(shù)量和服務(wù)員數(shù)量,即并發(fā)多進(jìn)程處理多個(gè)請(qǐng)求的概念。

但是這個(gè)飯店老板慢慢發(fā)現(xiàn),增加服務(wù)員和廚師的同時(shí),飯店的空間是有限的,慢慢的變得擁擠(阻塞),而且更為頭疼的是另一個(gè)問(wèn)題:服務(wù)員太悠閑了,2分鐘就把點(diǎn)菜的事干完了,廚師做菜10分鐘,那他就有8分鐘在那干等著,沒(méi)事干,因?yàn)閺N師沒(méi)把菜做完給他,他也不能接待下一個(gè)顧客。
同樣類(lèi)似于Apache開(kāi)發(fā)web時(shí)候,CPU分配的最大進(jìn)程數(shù)是有限的,并不能沒(méi)完沒(méi)了的分配進(jìn)程的,并發(fā)到一定數(shù)目的時(shí)候,必須得排隊(duì)(阻塞)了,更大的問(wèn)題是,CPU處理的速度遠(yuǎn)遠(yuǎn)快于I/O,在Web場(chǎng)景中,CPU運(yùn)用場(chǎng)景很少,大頭都在I/O上,CPU大部分進(jìn)程情況下都是在等待,等待I/O,CPU的資源被浪費(fèi)掉了,相當(dāng)于飯店的服務(wù)員一直干等著沒(méi)事干。
Node.js很好的解決了上面的問(wèn)題??????,來(lái)看下Node.js對(duì)應(yīng)的生活中的場(chǎng)景,如下:

另一個(gè)老板也開(kāi)了一家飯店,這家飯店只雇傭了一個(gè)服務(wù)員,這個(gè)服務(wù)員接待所有的顧客,顧客來(lái)了依次點(diǎn)菜,點(diǎn)完菜拿個(gè)號(hào)找地方坐著去了,然后服務(wù)員把菜單交給廚師們,然后再去給下一波客人點(diǎn)菜下單,等后廚什么時(shí)候說(shuō),服務(wù)員幾號(hào)的菜好了,然后服務(wù)員端好菜給制定號(hào)碼的顧客,無(wú)論哪個(gè)顧客來(lái)了立刻響應(yīng),廚師一直做菜,服務(wù)員一直接單,顧客的體驗(yàn)也比上一家要好,不用等到上一個(gè)顧客拿到菜才開(kāi)始點(diǎn)單等待。對(duì)應(yīng)Web體驗(yàn)就是:一直在那轉(zhuǎn)圈等待,要比直接顯示連不上要好。
同樣在Node.js中,用戶(hù)(顧客)發(fā)來(lái)請(qǐng)求,有一個(gè)主進(jìn)程(服務(wù)員),對(duì)應(yīng)有一個(gè)事件輪詢(xún)(Event Loop),來(lái)處理用戶(hù)的各種請(qǐng)求過(guò)來(lái)的進(jìn)程(菜單),如qq音樂(lè)(小蔥拌豆腐),Photoshop(魚(yú)香肉絲)等,然后給Worker threads即多線程(廚師)的去處理,處理完后完成回調(diào)(上菜),CPU的利用率達(dá)到最大。在Node.js中,一個(gè)CPU上只開(kāi)一個(gè)進(jìn)程,一個(gè)進(jìn)程里只有一個(gè)線程。

6、Node.js單線程
Node.js單線程指的是,一個(gè)CPU上只開(kāi)一個(gè)進(jìn)程,一個(gè)進(jìn)程里只有一個(gè)線程。但這個(gè)單線程只是針對(duì)主進(jìn)程,I/O 等其它各種異步操作都是操作系統(tǒng)底層在多線程調(diào)度的 。Node.js就是主進(jìn)程發(fā)起一個(gè)請(qǐng)求,請(qǐng)求交給I/O 之后,是由操作系統(tǒng)底層多進(jìn)程多線程進(jìn)行調(diào)度,然后達(dá)到最佳性能。
Node.js是單線程的,但是不要理解為它做所有事都是單線程的,有一部分不是自己做的,而是交給操作系統(tǒng)做的,它只負(fù)責(zé)單進(jìn)程的在那聽(tīng),操作系統(tǒng)好了,就告訴它單進(jìn)程的做另外的事情,操作系統(tǒng)怎么處理I/O,它不管。
單線程并不就是單進(jìn)程,Node.js有個(gè)多核處理模塊叫cluster,專(zhuān)門(mén)用來(lái)處理多CPU,CPU如果有8個(gè)核,用了cluster之后,Node.js就啟了8個(gè)進(jìn)程,不會(huì)浪費(fèi)CPU的能力。
7、Node.js能干嘛
- Web Server
- 本地代碼編譯構(gòu)建(grunt、babel等工具都是基于Node.js開(kāi)發(fā)的)
- 實(shí)用工具的開(kāi)發(fā)(爬蟲(chóng)等)
三、Node.js的基礎(chǔ)API
1、path(路徑)
path 模塊提供了一些工具函數(shù),用于處理文件與目錄的路徑??梢酝ㄟ^(guò)以下方式使用:
const path = require('path');
path常用方法:
① path.normalize(path)
會(huì)規(guī)范化給定的 path,并解析 '..' 和 '.' 片段,如:
const { normalize } = require('path');
console.log(normalize.('/usr///local/bin')); // /usr/local/bin
console.log(normalize.('/usr//local/../bin')); // /usr/bin
/*或者這樣寫(xiě):
const path = require('path');
console.log(path.normalize.('/usr///local/bin'));
*/
② path.join([...paths])
使用平臺(tái)特定的分隔符把全部給定的 path 片段連接到一起,并規(guī)范化生成的路徑,也能解析 '..' 和 '.' ,如:
const { join } = require('path');
console.log(join.('/usr', 'local', 'bin/')); // /usr/local/bin
console.log(join.('/usr', '../local', 'bin/')); // /usr/bin
③ path.resolve([...paths])
會(huì)把一個(gè)路徑或路徑片段的序列解析為一個(gè)絕對(duì)路徑,如:
const { resolve } = require('path');
console.log(resolve.('./')); // /Users/peng/Desktop 返回當(dāng)前路徑的絕對(duì)路徑
④ path.basename(path[, ext])
返回文件名
path.dirname(path) 返回所在文件夾名
path.extname(path) 返回?cái)U(kuò)展名
const { basename, dirname, extname } = require('path');
const filePath = '/usr/local/bin/test.html';
console.log(basename.(filePath)); // test.html
console.log(dirname.(filePath)); // /usr/local/bin
console.log(extname.(filePath)); // .html
⑤ path.parse(path)
返回一個(gè)對(duì)象,對(duì)象的屬性表示 path 的元素
path.format() 會(huì)從一個(gè)對(duì)象返回一個(gè)路徑字符串。 與 path.parse()方法相反
const { parse, format } = require('path');
const filePath = '/usr/local/bin/test.html';
const ret = parse(filePath);
console.log(ret);
/*
{ root: '/',
dir: '/usr/local/bin',
base: 'test.html',
ext: '.html',
name: 'test' }
*/
console.log(format(ret)); // /usr/local/bin/test.html
另外:
__dirname、__filename總是返回文件的絕對(duì)路徑
process.cwd()總是返回執(zhí)行node命令所在的文件夾
2、Buffer (緩沖)
Buffer 類(lèi)被引入作為 Node.js API 的一部分,使其可以在 TCP 流或文件系統(tǒng)操作等場(chǎng)景中處理二進(jìn)制數(shù)據(jù)流。
Buffer 類(lèi)的實(shí)例類(lèi)似于整數(shù)數(shù)組,但 Buffer 的大小是固定的、且在 V8 堆外分配物理內(nèi)存。 Buffer 的大小在被創(chuàng)建時(shí)確定,且無(wú)法調(diào)整。
Buffer 類(lèi)在 Node.js 中是一個(gè)全局變量(global),因此無(wú)需使用require('buffer').Buffer。
常用方法:
① Buffer.byteLength()
返回一個(gè)字符串的實(shí)際字節(jié)長(zhǎng)度。 這與 String.prototype.length不同,因?yàn)槟欠祷刈址?strong>字符數(shù)。
console.log(Buffer.byteLength('test')); // 4
console.log(Buffer.byteLength('中國(guó)')); // 6
② Buffer.from(array)
通過(guò)一個(gè)八位字節(jié)的 array 創(chuàng)建一個(gè)新的 Buffer ,如果 array 不是一個(gè)數(shù)組,則拋出 TypeError 錯(cuò)誤。
console.log(Buffer.from([1, 2, 3])); // <Buffer 01 02 03>
③ Buffer.isBuffer(obj)
如果 obj 是一個(gè) Buffer 則返回 true ,否則返回 false
console.log(Buffer.isBuffer({ 'a': 1 })); // false
console.log(Buffer.isBuffer(Buffer.from([1, 2, 3]))); // true
④ Buffer.concat(list)
如果 obj 是一個(gè) Buffer 則返回 true ,否則返回 false
const buf1 = Buffer.from('hello ');
const buf2 = Buffer.from('world');
const buf = Buffer.concat([buf1, buf2]);
console.log(buf.toString()); // hello world
常用屬性:
① buf.length 長(zhǎng)度
buf.toString() 轉(zhuǎn)為字符串
buf.fill() 填充
buf.equals() 判斷是否相等
buf.indexOf() 是否包含,如果包含返回位置值,不包含返回-1
const buf = Buffer.from('hello world');
const buf2 = Buffer.from('hello world!');
console.log(buf.length); // 15
console.log(buf.toString()); // hello world
console.log(buf.fill(10, 2, 6)); // <Buffer 68 65 0a 0a 0a 0a 77 6f 72 6c 64> 這里從第3個(gè)到第6個(gè)都被替換成了0a,a就是16進(jìn)制的數(shù)字10
console.log(buf.equals(buf2)); // false
console.log(buf.indexOf('h')); // 0
3、events(事件)
大多數(shù) Node.js 核心 API 都采用慣用的異步事件驅(qū)動(dòng)架構(gòu),其中某些類(lèi)型的對(duì)象(觸發(fā)器)會(huì)周期性地觸發(fā)命名事件來(lái)調(diào)用函數(shù)對(duì)象(監(jiān)聽(tīng)器)。
所有能觸發(fā)事件的對(duì)象都是 EventEmitter 類(lèi)的實(shí)例。 這些對(duì)象開(kāi)放了一個(gè) eventEmitter.on() 函數(shù),允許將一個(gè)或多個(gè)函數(shù)綁定到會(huì)被對(duì)象觸發(fā)的命名事件上。 事件名稱(chēng)通常是駝峰式的字符串,但也可以使用任何有效的 JavaScript 屬性名。
官網(wǎng)例子:一個(gè)綁定了一個(gè)監(jiān)聽(tīng)器的 EventEmitter 實(shí)例。 eventEmitter.on() 方法用于注冊(cè)監(jiān)聽(tīng)器,eventEmitter.emit() 方法用于觸發(fā)事件。
const EventEmitter = require('events');
class CustomEvent extends EventEmitter {}
const myEmitter = new CustomEvent();
myEmitter.on('error', err => {
console.log(err);
})
myEmitter.emit('error', new Error('This is an error!'));
當(dāng)有一個(gè)錯(cuò)誤的時(shí)候,會(huì)顯示Error: This is an error!,然后顯示具體錯(cuò)誤內(nèi)容。
4、fs(文件系統(tǒng))
通過(guò) require('fs') 使用該模塊。 所有的方法都有異步和同步的形式。
異步方法的最后一個(gè)參數(shù)都是一個(gè)回調(diào)函數(shù)。 傳給回調(diào)函數(shù)的參數(shù)取決于具體方法,但回調(diào)函數(shù)的第一個(gè)參數(shù)都會(huì)保留給異常。 如果操作成功完成,則第一個(gè)參數(shù)會(huì)是 null 或 undefined。
常用方法如下:
① fs.readFile(path[, options], callback)
異步地讀取一個(gè)文件的全部?jī)?nèi)容
const fs = require('fs');
fs.readFile('./test.txt', (err, data) => {
if (err) throw err;
console.log(data);
});
此時(shí)如果test.txt文件內(nèi)容只有一個(gè)字母a,那么打印出來(lái)的就是<Buffer 61>
回調(diào)有兩個(gè)參數(shù) (err, data),其中 data 是文件的內(nèi)容。如果未指定字符編碼,則返回原始的 buffer。
指定編碼格式后,就會(huì)按照編碼格式打印文件內(nèi)容:
const fs = require('fs');
fs.readFile('./test.txt', 'utf-8',(err, data) => {
if (err) throw err;
console.log(data);
});
此時(shí)如果test.txt文件內(nèi)容只有一個(gè)字母a,那么打印出來(lái)的就是a
② fs.writeFile(file, data[, options], callback)
異步地寫(xiě)入數(shù)據(jù)到文件,如果文件已經(jīng)存在,則替代文件。
const fs = require('fs');
fs.writeFile('message.txt', 'Hello Node.js', (err) => {
if (err) throw err;
console.log('The file has been saved!');
});
③ fs.stat(path,callback)
可用來(lái)判斷一個(gè)文件是否存在
回調(diào)有兩個(gè)參數(shù) (err, stats),其中 stats是一個(gè) fs.Stats對(duì)象。
const fs = require('fs');
fs.stat('./message.txt', (err, stats)=>{
if (err){
console.log('文件不存在');
return;
};
console.log(stats.isFile()); // true 判斷是否是一個(gè)文件
console.log(stats.isDirectory()); // false 判斷是否是一個(gè)文件夾
});
④ fs.rename(oldPath, newPath, callback)
用來(lái)修改文件名
const fs = require('fs');
fs.rename('./message.txt', 'm.txt', err=>{
if (err) throw err;
console.log('修改成功!');
})
⑤ fs.unlink(path, callback)
刪除文件
const fs = require('fs');
fs.unlink('./m.txt', err=>{
if (err) throw err;
console.log('刪除成功!');
})
⑥ fs.readdir(path[, options], callback)
讀取指定路徑下的所有文件
const fs = require('fs');
fs.readdir('./', (err, files)=>{
if (err) throw err;
console.log(files);
/*
[ '.DS_Store',
'node_modules',
'package.json',
'test.js',
'test.txt' ]
*/
})
⑦ fs.mkdir(path[, mode], callback)
在指定路徑里創(chuàng)建一個(gè)文件夾
const fs = require('fs');
// 在當(dāng)前目錄創(chuàng)建一個(gè)叫test的文件夾
fs.mkdir('./test', err=>{
if (err) throw err;
console.log('文件夾創(chuàng)建成功');
})
⑧ fs.rmdir(path, callback)
刪除指定路徑下的文件夾
const fs = require('fs');
fs.rmdir('./test', err=>{
if (err) throw err;
console.log('文件夾刪除成功');
})
⑨ fs.watch(filename[, options][, listener])
和gulp里的watch很像,用來(lái)監(jiān)視 filename的變化,filename 可以是一個(gè)文件或一個(gè)目錄。
監(jiān)聽(tīng)器回調(diào)有兩個(gè)參數(shù) (eventType, filename)。 eventType 可以是 'rename' 或 'change',filename 是觸發(fā)事件的文件的名稱(chēng)。
const fs = require('fs');
fs.watch('./', {
recursive: true // 指明是否全部子目錄應(yīng)該被監(jiān)視
}, (eventType, filename) =>{
console.log(eventType, filename);
})
注意,在大多數(shù)平臺(tái),當(dāng)一個(gè)文件出現(xiàn)或消失在一個(gè)目錄里時(shí),'rename' 會(huì)被觸發(fā)。
⑩ fs.createReadStream(path[, options])
返回一個(gè)新建的 ReadStream 對(duì)象
const fs = require('fs');
const rs = fs.createReadStream('./test.txt');
rs.pipe(process.stdout); // 在終端輸出test.txt內(nèi)容