1. 程式人生 > >前端修煉——Node.js(一)

前端修煉——Node.js(一)

最近在學習 Node,看的是樸靈老師編著的《深入淺出Node.js》。這本書和我看過的其他技術類書籍有些不同,書中不僅講解了 Node 的歷史,發展,特點,應用場景等等。還說明了很多原理,讓我有一種“我靠~原來是這樣實現的啊!”,恍然大悟的感覺。作為一名前端程式猿,也經常使用比如 webpack、gulp、vue-cli …這些自動化的前端工具,他們的開發或環境也是基於 node啊,通過這本書理解了很多之前並不是很理解的問題。Node 的橫空出世也將前端推上了巔峰,感謝樸靈老師和其他本書的支持者,能讓我更好的學習 Node!

因為我本身是一名前端開發人員,所以對 Node 學習和理解,基本是從前端角度去理解的,如果有錯誤或誤解的地方,歡迎各位及時指出更正。

Node 給 JavaScript 帶來的意義

我們知道瀏覽器中除了 V8 作為 JavaScript 的引擎外,還有一個 WebKit 佈局引擎(這個還不太清楚),HTML5的發展中,定義了更多更豐富的 API ,瀏覽器提供了越來越多的功能暴露給 JavaScript 和 HTML 標籤。但長久以來,JavaScript 被限制在瀏覽器的沙箱中執行,它的能力取決於瀏覽器的中間層提供的支援有多少。也就是一直受瀏覽器限制,做不了啥“大事”,不能訪問本地檔案,不能操作資料庫。

但是在 Node 中,JavaScript 可以隨心所欲的訪問本地檔案,搭建伺服器端,連線資料庫。而且 Node 的語法和 JavaScript 的語法是一樣的,也就是說 JS 程式碼,可以實現後端的一些功能,這就牛X了有沒有。

Node 的特點

Node 的特點有三個:

  • 非同步 I/O
  • 事件與回撥函式
  • 單執行緒

其實瞭解了這三個特點後,你會覺得這三個差不多是一個特點,只是不同階段的不同特徵。

非同步 I/O

作為前端工程師,對於非同步的理解可以說的小菜一碟了,例如我們和 Ajax 基本是擡頭不見低頭見了:

$.post('url',{title:'深入淺出Node.js'},function(data){
    console.log(111);
});
console.log(222);

前端開發的肯定知道,“111”是在“222”之後輸出的。在 Ajax 執行以後,是直接向下執行

“222”的,並不會等待 Ajax 執行完,只知道“111”會在請求成功之後才執行,但具體時間未知。這就是一個典型的非同步 I/O 操作。

在 Node 中非同步呼叫和 Ajax 基本差不多,以讀取檔案為例:

var fs = require('fs');
fs.readFile('path',function(err,file){
    console.log(111);
});
console.log(222);

這裡的**“111”也是在“222”**之後輸出的。

在 Node 中絕大多數的操作都是非同步呼叫的,這樣極大的提升了效率。

事件與回撥函式

前端開發中,回撥函式無處不在,Node 中也是如此。從上面的例子可以看出,Ajax 中 success 就是回撥。除了非同步和事件外,回撥也是 Node 的一大特色。

單執行緒

Node 保留了 JavaScript 在瀏覽器中單執行緒的特點。單執行緒的最大好處是不用擔心多執行緒中的狀態同步的問題,Node 中沒有死鎖的存在,也沒有執行緒上下文交換帶來效能上的開銷。

但是單執行緒也有缺點:

  • 無法利用多核 CPU
  • 錯誤會引起整個應用崩潰,健壯性不足
  • 大量計算佔用 CPU 無法繼續呼叫非同步 I/O

後面,書中也講解了如何避免這些弱點,更高效的使用 Node.js。

Node 應用場景

說到應用只有兩種結果,適合與不適合。探討較多的就是 I/O 密集型CPU 密集型

I/O 密集型自然不用多說,Node 對 I/O 的處理能力還是很6的。至於計算方面的 CPU 密集型,有兩種方法:

  • 通過編寫 C/C++ 擴充套件模組,充分利用 CPU
  • 通過子程序方式分擔計算任務

這裡有一句精華:CPU 密集不可怕,如何合理排程是訣竅!

上面的內容是從大體上了解 Node,它的特點和應用場景。下面將會從各個相關模組,來深入認識 Node,以及各方面的實現原理。

模組機制

CommonJS 規範

CommonJS 規範為 JavaScript 制定了一個美好的願景——希望 JavaScript 能在任何地方執行。

CommonJS 規範的提出,主要是為了彌補後端 JavaScript 沒有標準的缺陷,已達到像 Python,Ruby 和 Java 具備開發大型應用的基礎能力,而不是停留在小指令碼程式的階段。讓它不僅可以開發客戶端應用,還可以開發:

  • 伺服器端 JavaScript 應用程式
  • 命令列工具
  • 桌面圖形應用程式
  • 混合應用
CommonJS模組規範

主要分為:模組引用模組定義模組標識 3個部分。
require('moduleName')引入模組。
exports.moduleNameexports 物件用於到處當前模組的方法或變數,是唯一的匯出出口。
模組標識就是傳遞給 require()方法的引數,它必須是小駝峰命名的字串,或者相對路徑,或絕對路徑,可以省略 .js 字尾。
每個模組具有獨立的空間,塊之間互不干擾。
require 的尋找機制,是沿路徑向上逐級遞迴,直到根目錄下的 node_modules 目錄

副檔名分析

require 在分析識別符號的過程中,如果沒寫上副檔名,Node 會按 .js .node .json 的順序依次嘗試。
在嘗試的過程中,需要呼叫 fs 模組同步阻塞地判斷檔案存不存在,這樣會引起效能問題。小訣竅就是:如果是 .node.json 結尾的檔案,引入時名稱就不要省略字尾了,加上就 OK 啦。

目錄分析和包

如果 require() 引入時,寫的不是檔名,是目錄名,此時 Node 會把目錄當做是一個包來處理,然後在當前目錄下查詢 package.json 檔案,通過JSON.parse()解析出包描述物件,從 main 屬性指定的檔名進行定位。

如果連 package.json 檔案都沒有,Node 會將檔名預設為 index ,依次查詢 index.jsindex.nodeindex.json 檔案。

如果找了一圈這些也沒找著,那 Node 可就不樂意了,就該給你扔個錯誤出來了(not found module…)。

JavaScript模組的編譯

有幾個疑惑點,每個模組裡都存在 requireexportsmodule 這3個變數,但是在模組中並沒有定義,他們從何而來?

還有在 Node 的 API 文件中,每個模組還有 __filename__dirname 這兩個變數,他們又從哪來的?

原因是,在編譯過程中,Node 對獲取的 JavaScript 檔案內容進行了頭尾包裝。在頭部添加了“(function(exports,require,module,__filename,__dirname){\n”,在尾部添加了“\n})”。

(function(exports,require,module,__filename,__dirname){
    var math = require('math');
    exports.area = function(radius){
        return Math.PI * radius * radius;
    };
});

也就是 Node 把引入的 js 檔案內容包在了一個函式裡,這樣就形成了一個封閉作用域,不會對其他模組造成汙染,這就是 Node 對 CommonJS 規範的實現。

還有就是為何存在 exports,也存在 module.exports,給 exports 賦值卻失敗的情況。

exports = function(){
  // 這樣是不行的
};

原因在於,exports 物件是通過形參方式傳入的,直接賦值會改變形參的作用,但並不能改變作用域外的值。

var change = function(a){
    // 直接使用exports.a更改相當於在這裡改a的值,改不了外面的a
    a = 100;
    console.log(a); // 100
};
// 使用module.export.a 就可以改這個a的值
var a = 10;
change(a);
console.log(a); // 10
核心模組

Node 自帶的模組就是核心模組。

核心模組分為 C/C++ 編寫JavaScript 編寫 兩部分。由 C/C++ 編寫的模組也叫做內建模組

在編譯 C/C++ 檔案之前,會把 JavaScript 模組檔案編譯成 C/C++ 程式碼,將所有內建的 JavaScript 程式碼轉換成 C++ 裡的陣列,JavaScript 程式碼以字串形式儲存在 node 名稱空間中。(此時是不可執行的

Node 執行時,JavaScript 核心模組,經過 頭尾包裝 後匯出,得以引入。(此時是可執行的

內建模組會編譯成二進位制檔案,一旦 Node 執行,它們會直接載入進記憶體當中,直接就可執行。

在 Node 所有的模組型別中,存在著一種依賴關係,即檔案模組可能依賴核心模組,核心模組可能依賴內建模組。 (檔案模組即第三方自己下載的模組

通常,檔案模組不推薦直接呼叫內建模組。如需呼叫,直接呼叫核心模組即可。

Node 在啟動時,會生成一個全域性變數 process,並提供 Binding() 方法來協助載入來載入內建模組。所以核心模組中基本都封裝了內建模組。

核心模組引入流程

為了符合 CommonJS 模組規範,從 JavaScript 到 C/C++ 的過程是很複雜的,它要經歷 C/C++ 層面的內建模組定義(JavaScript)核心模組的定義和引入(JavaScript)檔案模組層面的引入

但是使用 require()就十分簡潔、友好了。

編寫核心模組

本人功力還不到火候,這一節就直接跳過了,有能力的朋友可自行研究啦。

模組呼叫棧

C/C++ 內建模組屬於最底層模組,它屬於核心模組,主要提供 API 給 JavaScript 核心模組和第三方的檔案模組呼叫。

JavaScript 核心模組主要有兩個職責:

  • 作為C/C++ 內建模組的封裝層和橋接層,供檔案模組呼叫
  • 純粹的功能模組

檔案模組就是第三方模組,自己編寫的模組,不要被它搞暈呦。

包與NPM

Node 有自帶的模組,但是開發中肯定不夠用啊!所以有世界各地大牛們自己開發了很多的包,但是第三方的包是分散在世界各地的,相互不能引用。這時候,包和NPM就發揮作用了,它就是將模組聯絡起來的一種機制。

CommonJS 的包規範有兩個部分:

  • 包結構:組織包中的各種檔案
  • 包描述檔案:包的相關資訊(就是那個package.json檔案)
NPM 常用功能

我經常使用的就是npm install 包名稱 -S下載一些包,npm run dev 執行一些命令,具體其他更多功能可以百度一下啦(太懶了太懶了)。

Node 通過 CommonJS 規範,組織了自身的原生模組,彌補 JavaScript 弱結構問題,形成了穩定的結構,並向外提供服務。
NPM 組織了第三方模組,使專案開發中的依賴問題得到解決,藉助第三方開源力量,也發展了自己,形成一個良性的生態系統。

未完待續。。。。。。


文章只是本人學習 Node 過程中,按自己的理解總結的一些筆記,若有錯誤之處,歡迎各位及時指出,一起探討。

微信搜尋公眾號:前端很忙

做一個喜歡分享的前端開發者!

獲取更多幹貨分享,歡迎來搞!