Node.js自學完全總結


零、什么是Node.js?

引用Node.js官方網站的解釋如下:

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 是一個基於 Chrome V8 引擎的 JavaScript 運行環境
Node.js 使用了一個事件驅動非阻塞式 I/O 的模型,使其輕量又高效。

1、 運行環境(Runtime)

如果做一個類比,Node.js與JavaScript關系,就像JDK(Java Development Kit)與Java的關系。
總的來說,Node.js不是一門語言,而是用來進行Web開發的Runtime。

2、事件驅動(Event-driven)

在前端web開發中比較常見的事件驅動例子是,給一個按鈕綁定一個事件處理程序,這個事件處理程序就是事件驅動的,JavaScript進程並不知道什么時候調用它,點擊按鈕,觸發Click事件,此時主程序得到相應的通知,就知道調用綁定的的事件處理程序了。
因為Node.js是JavaScript的Runtime,所以天然就可以使用這種模式通知主進程的I/O 完成。

3、非阻塞式 I/O(Non-blocking I/O)

阻塞:I/O 時進程休眠等待 I/O 完成后進行下一步
非阻塞:I/O 時函數立即返回,進程不等待I/O 完成

一、Node.js 究竟好在哪里?

1、為什么偏愛Node.js

① 前端需求變得重要、職責范圍變大,統一開發體驗
② 在處理高並發、I/O 密集型場景性能優勢明顯

Node.js 使用了事件驅動和非阻塞的 I/O 模型,使 Node 輕量高效,非常適合 I/O 密集的 Web 場景。

CPU密集型 VS I/O密集型
CPU密集型:計算等邏輯判斷的操作,如:壓縮、解壓、加密和解密等。
I/O 密集型:存取設備,網絡設施的讀取操作,如:文件的存取,http等網絡操作,數據庫操作等。

2、Web常見場景

① 靜態資源讀取
html,css,js等文件的讀取
② 數據庫操作
把數據存取到物理設磁盤或內存中
③ 渲染頁面
讀取模板文件,根據數據生成html

3、高並發應對之道

高並發,簡而言之就是單位時間內訪問量特別大。

對應生活中的場景,一家菜館做菜招待顧客,老板剛開始就雇了一個廚師,做菜好吃不貴,顧客很多,顧客排好一條隊,然后顧客選好菜,廚師拿到菜單開始做菜,做好菜,給顧客端上來,再招待下個顧客。

 
一個廚師應對若干顧客

客人增多,一個廚師忙不過來了,老板於是又招了2個廚師,這樣顧客可以排3條隊,快了很多。

 
多個廚師應對若干顧客

隨着菜館名氣增大,顧客越來越多,老板本想再用之前的方法多招幾個廚師,但是老板想,多招廚師好像不太划算,有些廚師做飯快,有些做飯慢,經過調查,老板發現做飯快2倍的廚師只需要花費原來廚師工資的1.5倍,於是精明的老板炒掉了原來的3個廚師,招來了比原來廚師做飯速度快2倍的另外3個廚師,菜館比之前運轉的更好了。

 
多個速度快的廚師應對若干顧客

回到Web開發場景,廚師就是物理服務器,應對高並發的方法如下:① 增加機器數
機器多了,流量還是一樣的大,通過Nginx負載均衡分到不同的機器上處理
② 增加每台機器的CPU數——多核
單位機器,核數增多,運算能力增強了

4、進程與線程

進程在百度百科中的解釋如下:

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位。

換成正常的人話就是:電腦桌面的程序,如QQ音樂,當我們雙擊圖標時,實際上是把這個程序加載到內存中執行,我們稱這個執行中的程序就是進程,操作系統都是用進程作為基本單位進行分配和調度的。

多進程:啟動多個進程,多個進程可以一塊執行多個任務。

線程,進程內一個相對獨立的、可調度的執行單元,與同屬一個進程的線程共享進程的資源。

多線程,啟動一個進程,在一個進程內啟動多個線程,這樣,多個線程也可以一塊執行多個任務。

5、Node.js工作模型

傳統的server處理請求(如多線程高並發模式的Apache)對應生活中的場景,如下:
一個老板開了一家飯店,不同於之前那個菜館,這家的每個廚師配備了一個服務員,專門負責點菜,然后把菜單給廚師,廚師負責做菜,做完后給服務員,服務員端給客人,然后再接待顧客隊伍中的下一個。

如果這個飯店是Web的話,點菜這個動作很快,相當於CPU的運算,如訪問一個靜態資源,CPU運算后知道是哪個文件了,去相應盤讀取,類似於廚師做飯,是一個相對較慢的阻塞I/O操作,當顧客很多時候就相當於高並發了。忙不過來的時候,可以選擇增加廚師數量和服務員數量,即並發多進程處理多個請求的概念。

 
一個服務員每次只接待一個顧客

但是這個飯店老板慢慢發現,增加服務員和廚師的同時,飯店的空間是有限的,慢慢的變得擁擠(阻塞),而且更為頭疼的是另一個問題:服務員太悠閑了,2分鍾就把點菜的事干完了,廚師做菜10分鍾,那他就有8分鍾在那干等着,沒事干,因為廚師沒把菜做完給他,他也不能接待下一個顧客。

同樣類似於Apache開發web時候,CPU分配的最大進程數是有限的,並不能沒完沒了的分配進程的,並發到一定數目的時候,必須得排隊(阻塞)了,更大的問題是,CPU處理的速度遠遠快於I/O,在Web場景中,CPU運用場景很少,大頭都在I/O上,CPU大部分進程情況下都是在等待,等待I/O,CPU的資源被浪費掉了,相當於飯店的服務員一直干等着沒事干。

Node.js很好的解決了上面的問題👆👆👆,來看下Node.js對應的生活中的場景,如下:

 
一個服務員每次接待多個顧客

另一個老板也開了一家飯店,這家飯店只雇佣了一個服務員,這個服務員接待所有的顧客,顧客來了依次點菜,點完菜拿個號找地方坐着去了,然后服務員把菜單交給廚師們,然后再去給下一波客人點菜下單,等后廚什么時候說,服務員幾號的菜好了,然后服務員端好菜給制定號碼的顧客,無論哪個顧客來了立刻相應,廚師一直做菜,服務員一直接單,顧客的體驗也比上一家要好,不用等到上一個顧客拿到菜才開始點單等待。對應Web體驗就是:一直在那轉圈等待,要比直接顯示連不上要好。

同樣在Node.js中,用戶(顧客)發來請求,有一個主進程(服務員),對應有一個事件輪詢(Event Loop),來處理用戶的各種請求過來的進程(菜單),如qq音樂(小蔥拌豆腐),Photoshop(魚香肉絲)等,然后給Worker threads即多線程(廚師)的去處理,處理完后完成回調(上菜),CPU的利用率達到最大。在Node.js中,一個CPU上只開一個進程,一個進程里只有一個線程

 
Node.js工作模型(圖片源於網絡)
6、Node.js單線程

Node.js單線程指的是,一個CPU上只開一個進程,一個進程里只有一個線程。但這個單線程只是針對主進程,I/O 等其它各種異步操作都是操作系統底層在多線程調度的 。Node.js就是主進程發起一個請求,請求交給I/O 之后,是由操作系統底層多進程多線程進行調度,然后達到最佳性能。

Node.js是單線程的,但是不要理解為它做所有事都是單線程的,有一部分不是自己做的,而是交給操作系統做的,它只負責單進程的在那,操作系統好了,就告訴它單進程的做另外的事情,操作系統怎么處理I/O,它不管。

單線程並不就是單進程,Node.js有個多核處理模塊叫cluster,專門用來處理多CPU,CPU如果有8個核,用了cluster之后,Node.js就啟了8個進程,不會浪費CPU的能力。

7、Node.js能干嘛
  • Web Server
  • 本地代碼編譯構建(grunt、babel等工具都是基於Node.js開發的)
  • 實用工具的開發(爬蟲等)

三、Node.js的基礎API

1、path(路徑)

path 模塊提供了一些工具函數,用於處理文件與目錄的路徑。可以通過以下方式使用:
const path = require('path');
path常用方法:
path.normalize(path)
會規范化給定的 path,並解析 '..' 和 '.' 片段,如:

const { normalize } = require('path'); console.log(normalize.('/usr///local/bin')); // /usr/local/bin console.log(normalize.('/usr//local/../bin')); // /usr/bin /*或者這樣寫: const path = require('path'); console.log(path.normalize.('/usr///local/bin')); */ 

path.join([...paths])
使用平台特定的分隔符把全部給定的 path 片段連接到一起,並規范化生成的路徑,也能解析 '..' 和 '.' ,如:

const { join } = require('path'); console.log(join.('/usr', 'local', 'bin/')); // /usr/local/bin console.log(join.('/usr', '../local', 'bin/')); // /usr/bin 

path.resolve([...paths])
會把一個路徑或路徑片段的序列解析為一個絕對路徑,如:

const { resolve } = require('path'); console.log(resolve.('./')); // /Users/peng/Desktop 返回當前路徑的絕對路徑 

path.basename(path[, ext])
返回文件名
path.dirname(path) 返回所在文件夾名
path.extname(path) 返回擴展名

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)
返回一個對象,對象的屬性表示 path 的元素
path.format() 會從一個對象返回一個路徑字符串。 與 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總是返回文件的絕對路徑
process.cwd()總是返回執行node命令所在的文件夾

2、Buffer (緩沖)

Buffer 類被引入作為 Node.js API 的一部分,使其可以在 TCP 流或文件系統操作等場景中處理二進制數據流。

Buffer 類的實例類似於整數數組,但 Buffer 的大小是固定的、且在 V8 堆外分配物理內存。 Buffer 的大小在被創建時確定,且無法調整。

Buffer 類在 Node.js 中是一個全局變量(global),因此無需使用require('buffer').Buffer

常用方法:
Buffer.byteLength()
返回一個字符串的實際字節長度。 這與 String.prototype.length不同,因為那返回字符串的字符數。

console.log(Buffer.byteLength('test')); // 4 console.log(Buffer.byteLength('中國')); // 6 

Buffer.from(array)
通過一個八位字節的 array 創建一個新的 Buffer ,如果 array 不是一個數組,則拋出 TypeError 錯誤。

console.log(Buffer.from([1, 2, 3])); // <Buffer 01 02 03> 

Buffer.isBuffer(obj)
如果 obj 是一個 Buffer 則返回 true ,否則返回 false

console.log(Buffer.isBuffer({ 'a': 1 })); // false console.log(Buffer.isBuffer(Buffer.from([1, 2, 3]))); // true 

Buffer.concat(list)
如果 obj 是一個 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 長度
buf.toString() 轉為字符串
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個到第6個都被替換成了0a,a就是16進制的數字10 console.log(buf.equals(buf2)); // false console.log(buf.indexOf('h')); // 0 
3、events(事件)

大多數 Node.js 核心 API 都采用慣用的異步事件驅動架構,其中某些類型的對象(觸發器)會周期性地觸發命名事件來調用函數對象(監聽器)。

所有能觸發事件的對象都是 EventEmitter 類的實例。 這些對象開放了一個 eventEmitter.on() 函數,允許將一個或多個函數綁定到會被對象觸發的命名事件上。 事件名稱通常是駝峰式的字符串,但也可以使用任何有效的 JavaScript 屬性名。

官網例子:一個綁定了一個監聽器的 EventEmitter 實例。 eventEmitter.on() 方法用於注冊監聽器,eventEmitter.emit() 方法用於觸發事件。

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!')); 

當有一個錯誤的時候,會顯示Error: This is an error!,然后顯示具體錯誤內容。

4、fs(文件系統)

通過 require('fs') 使用該模塊。 所有的方法都有異步和同步的形式。

異步方法的最后一個參數都是一個回調函數。 傳給回調函數的參數取決於具體方法,但回調函數的第一個參數都會保留給異常。 如果操作成功完成,則第一個參數會是 null 或 undefined。

常用方法如下:
fs.readFile(path[, options], callback)
異步地讀取一個文件的全部內容

const fs = require('fs'); fs.readFile('./test.txt', (err, data) => { if (err) throw err; console.log(data); }); 

此時如果test.txt文件內容只有一個字母a,那么打印出來的就是<Buffer 61>
回調有兩個參數 (err, data),其中 data 是文件的內容。如果未指定字符編碼,則返回原始的 buffer。
指定編碼格式后,就會按照編碼格式打印文件內容:

const fs = require('fs'); fs.readFile('./test.txt', 'utf-8',(err, data) => { if (err) throw err; console.log(data); }); 

此時如果test.txt文件內容只有一個字母a,那么打印出來的就是a

fs.writeFile(file, data[, options], callback)
異步地寫入數據到文件,如果文件已經存在,則替代文件。

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)
可用來判斷一個文件是否存在
回調有兩個參數 (err, stats),其中 stats是一個 fs.Stats對象。

const fs = require('fs'); fs.stat('./message.txt', (err, stats)=>{ if (err){ console.log('文件不存在'); return; }; console.log(stats.isFile()); // true 判斷是否是一個文件 console.log(stats.isDirectory()); // false 判斷是否是一個文件夾 }); 

fs.rename(oldPath, newPath, callback)
用來修改文件名

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)
在指定路徑里創建一個文件夾

const fs = require('fs'); // 在當前目錄創建一個叫test的文件夾 fs.mkdir('./test', err=>{ if (err) throw err; console.log('文件夾創建成功'); }) 

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很像,用來監視 filename的變化,filename 可以是一個文件或一個目錄

監聽器回調有兩個參數 (eventType, filename)。 eventType 可以是 'rename' 或 'change',filename 是觸發事件的文件的名稱。

const fs = require('fs'); fs.watch('./', { recursive: true // 指明是否全部子目錄應該被監視 }, (eventType, filename) =>{ console.log(eventType, filename); }) 

注意,在大多數平台,當一個文件出現或消失在一個目錄里時,'rename' 會被觸發。

fs.createReadStream(path[, options])
返回一個新建的 ReadStream 對象

const fs = require('fs'); const rs = fs.createReadStream('./test.txt'); rs.pipe(process.stdout); // 在終端輸出test.txt內容



作者:JokerPeng
鏈接:https://www.jianshu.com/p/22f62a08559f
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM