這篇文章帶大家了解一下Node中的檔案模組和核心模組,聊聊檔案模組的尋找和檔案模組的編譯執行、JavaScript與C/C 核心模組的編譯執行,希望對大家有幫助!
在我們使用Nodejs 進行日常開發時,經常會使用require 導入兩類模組,一類是我們自己寫的模組或使用npm安裝的第三方模組,這類模組在Node 中稱為檔案模組
;另一類別則是Node 內建的提供給我們使用的模組,如os
、 fs
等模組,這些模組稱為核心模組
。
要注意的是,檔案模組與核心模組的差異不僅僅在於是否被 Node 內置,具體到模組的檔案定位、編譯和執行過程,兩者之間都存在明顯的差別。不僅如此,檔案模組還可以細分為普通檔案模組、自訂模組或 C/C 擴充模組等等,不同的模組在檔案定位、編譯等流程也存在諸多細節上的差異。
本文會就這些問題,理清文件模組與核心模組的概念以及它們在文件定位、編譯或執行等流程的具體過程和需要注意的細節,希望對你有所幫助。
我們先從檔案模組講起。
什麼是檔案模組呢?
在 Node 中,使用 .、.. 或 /
開頭的模組識別碼(也就是使用相對路徑或絕對路徑)來 require 的模組,都會被當作檔案模組。另外,還有一類特殊的模組,雖然不含有相對路徑或絕對路徑,也不是核心模組,但是會指向一個包,Node 在定位這類模組時,會用模組路徑
逐個查找該模組,這類模組被稱為自訂模組。
因此,檔案模組包含兩類,一類是帶路徑的普通檔案模組,一類是不帶路徑的自訂模組。
檔案模組在運行時動態加載,需要完整的檔案定位、編譯執行過程,速度比核心模組慢。
對於檔案定位而言,Node 對這兩類檔案模組的處理有所不同。我們來具體看看這兩類文件模組的查找流程。
對於普通的文件模組,由於攜帶路徑,指向非常明確,查找耗時不會很久,因此查找效率比下文要介紹的自訂模組要高一些。不過還是有兩點要注意。
一是通常情況下,使用 require 引入檔案模組時一般都不會指定檔案副檔名,例如:
const math = require("math");
由於沒有指定副檔名,Node 還不能確定最終的檔案。在這種情況下,Node 會依照 .js、.json、.node
的順序補足副檔名,依序嘗試,這個過程稱為 檔案副檔名分析
。
另外要注意的是,在實際開發中,除了require 一個特定的檔案外,我們通常還會指定一個目錄,例如:
const axios = require("../network");
在這種情況下,Node 會先進行文件副檔名分析,如果沒有查找到對應文件,但得到了一個目錄,此時Node 會將該目錄當作一個包來處理。
具體而言,Node 會將目錄中的 package.json
的 main
欄位所指向的檔案作為查找結果傳回。如果main 所指向的文件錯誤,或是壓根不存在package.json
文件,Node 會使用index
作為預設檔名,然後依序使用.js
、 .node
進行副檔名分析,逐一尋找目標文件,如果沒有找到的話就會拋出錯誤。
(當然,由於Node 存在兩類模組系統CJS 和ESM,除了查找main 字段外,Node 還會採用其他方式,由於不在本文討論範圍內,就不再贅述了。)
剛才提到,Node 在尋找自訂模組的過程中,會使用到模組路徑,那什麼是模組路徑呢?
熟悉模組解析的朋友應該都知道,模組路徑是一個由路徑組成的數組,具體的值可以看以下這個範例:
// example.js console.log(module.paths);
列印結果:
可以看到,Node 中的模組存在一個模組路徑數組,存放在module.paths
中,用於規定Node 如何找到目前模組引用的自訂模組。
具体来讲,Node 会遍历模块路径数组,逐个尝试其中的路径,查找该路径对应的 node_modules
目录中是否有指定的自定义模块,如果没有就向上逐级递归,一直到根目录下的 node_modules
目录,直到找到目标模块为止,如果找不到的话就会抛出错误。
可以看出,逐级向上递归查找 node_modules
目录是 Node 查找自定义模块的策略,而模块路径便是这个策略的具体实现。
同时我们也得出一个结论,在查找自定义模块时,层级越深,相应的查找耗时就会越多。因此相比于核心模块和普通的文件模块,自定义模块的加载速度是最慢的。
当然,根据模块路径查找到的仅仅是一个目录,并不是一个具体的文件,在查找到目录后,同样地,Node 会根据上文所描述的包处理流程进行查找,具体过程不再赘述了。
以上是普通文件模块和自定义模块的文件定位的流程和需要注意的细节,接下来我们来看者两类模块是如何编译执行的。
当定位到 require 所指向的文件后,通常模块标识符都不带有扩展名,根据上文提到的文件扩展名分析我们可以知道,Node 支持三种扩展名文件的编译执行:
JavaScript 文件。通过 fs
模块同步读取文件后编译执行。除了 .node
和 .json
文件,其他文件都会被当作 .js
文件载入。
.node
文件,这是用 C/C++ 编写后编译生成的扩展文件,Node 通过 process.dlopen()
方法加载该文件。
json 文件,通过 fs
模块同步读取文件后,使用 JSON.parse()
解析并返回结果。
在对文件模块进行编译执行之前,Node 会使用如下所示的模块封装器对其进行包装:
(function(exports, require, module, __filename, __dirname) { // 模块代码 });
可以看到,通过模块封装器,Node 将模块包装进函数作用域中,与其他作用域隔离,避免变量的命名冲突、污染全局作用域等问题,同时,通过传入 exports、require 参数,使该模块具备应有的导入与导出能力。这便是 Node 对模块的实现。
了解了模块封装器后,我们先来看 json 文件的编译执行流程。
json 文件的编译执行是最简单的。在通过 fs
模块同步读取 JSON 文件的内容后,Node 会使用 JSON.parse() 解析出 JavaScript 对象,然后将它赋给该模块的 exports 对象,最后再返回给引用它的模块,过程十分简单粗暴。
在使用模块包装器对 JavaScript 文件进行包装后,包装之后的代码会通过 vm
模块的 runInThisContext()
(类似 eval) 方法执行,返回一个 function 对象。
然后,将该 JavaScript 模块的 exports、require、module 等参数传递给这个 function 执行,执行之后,模块的 exports 属性被返回给调用方,这就是 JavaScript 文件的编译执行过程。
在讲解 C/C++ 扩展模块的编译执行之前,先介绍一下什么是 C/C++ 扩展模块。
C/C++ 扩展模块属于文件模块中的一类,顾名思义,这类模块由 C/C++ 编写,与 JavaScript 模块的区别在于其加载之后不需要编译,直接执行之后就可以被外部调用了,因此其加载速度比 JavaScript 模块略快。相比于用 JS 编写的文件模块,C/C++ 扩展模块明显更具有性能上的优势。对于 Node 核心模块中无法覆盖的功能或者有特定的性能需求,用户可以编写 C/C++ 扩展模块来达到目的。
那 .node
文件又是什么呢,它跟 C/C++ 扩展模块有什么关系?
事实上,编写好之后的 C/C++ 扩展模块经过编译之后就生成了 .node
文件。也就是说,作为模块的使用者,我们并不直接引入 C/C++ 扩展模块的源代码,而是引入 C/C++ 扩展模块经过编译之后的二进制文件。因此,.node
文件并不需要编译,Node 在查找到 .node
文件后,只需加载和执行该文件即可。在执行的过程中,模块的 exports 对象被填充,然后返回给调用者。
值得注意的是,C/C 擴充模組編譯產生的.node
檔案在不同平台下有不同的形式:在*nix
系統下C/C 擴充模組被g /gcc 等編譯器編譯為動態連結共享物件文件,擴展名為.so
;在Windows
下則被Visual C 編譯器編譯為動態連結庫文件,擴展名為.dll
。但在我們實際使用時使用的副檔名卻是.node
,事實上.node
的副檔名只是為了看起來更自然一點,實際上,在Windows
下它是一個.dll
文件,在*nix
下則是一個.so
檔案。
Node 在查找到要 require 的 .node
檔案之後,會呼叫 process.dlopen()
方法對該檔案進行載入和執行。由於.node
檔案在不同平台下是不同的檔案形式,為了實作跨平台,dlopen()
方法在Windows
和*nix
平台下分別有不同的實現,然後透過libuv
相容層進行封裝。下圖是C/C 擴充模組在不同平台下編譯和載入的過程:
process.binding() 透過模組標識符分析定位到其在記憶體中的位置,將其取出。取出後,JavaScript 核心模組同樣會經歷模組包裝器的包裝,然後被執行,導出 exports 對象,返回給呼叫者。
buffer、
fs、
os 等模組都是部分通過C/ C 編寫的。這種
C 模組主內完成核心,JavaScript 模組主外實作封裝的模式是 Node 提高效能的常見方式。
node_fs、
node_os 等,它們通常不會被使用者直接調用,而是被JavaScript 核心模組直接依賴。因此,在 Node 的核心模組的引入過程中,存在這樣一條引用鏈:
process.binding() 方法嗎,Node 透過呼叫該方法實作將 JavaScript 核心模組從記憶體中取出。此方法同樣適用於 JavaScript 核心模組,來協助載入內建模區塊。
get_builtin_module() 方法取出內建模區塊對象,透過執行
register_func () 填滿exports 對象,最後返回給呼叫方完成匯出。這就是內建模區塊的載入和執行過程。
##總結來說,引入os 模組的過程經歷JavaScript 檔案模組的引入、JavaScript 核心模組的載入和執行和內建模區塊的載入執行,過程十分繁瑣複雜,但是對於模組的呼叫者來說,由於屏蔽了底層的複雜實作和細節,光是透過require() 就可完成整個模組的導入,十分簡潔。友好。
本文介紹了文件模組與核心模組的基本概念以及它們在文件定位、編譯或執行等流程的具體過程和需要注意的細節。具體而言:
檔案模組根據檔案定位過程的差異可以分為普通檔案模組和自訂模組。普通檔案模組由於路徑明確,可以直接定位,有時會涉及到檔案副檔名分析、目錄分析的過程;自訂模組會根據模組路徑進行查找,查找成功之後也會透過目錄分析進行最終的檔案定位。
檔案模組根據編譯執行流程的不同可以分為 JavaScript 模組和 C/C 擴充模組。 JavaScript 模組被模組封裝器包裝之後透過vm
模組的runInThisContext
方法執行;C/C 擴充模組由於已經是經過編譯之後產生的可執行文件,因此可直接執行,返回導出物件給呼叫方。
核心模組分為 JavaScript 核心模組和內建模區塊。 JavaScript 核心模組在Node 程序啟動時便被載入進記憶體中,透過process.binding()
方法可將其取出,然後執行;內建模區塊的編譯執行會經歷process.binding ()
、get_builtin_module()
和register_func()
函數的處理。
除此之外,我們也得到了Node 引入核心模組的引用鏈,也就是檔案模組-->JavaScript 核心模組-->內建模區塊,也學習了C 模組主內完成核心,JavaScript 模組主外實作封裝的模組編寫方式。
更多程式相關知識,請造訪:程式設計影片! !
以上是一文了解Node中的檔案模組和核心模組的詳細內容。更多資訊請關注PHP中文網其他相關文章!