摘要:例如指定一些依賴到模塊中實現(xiàn)規(guī)范的模塊化,感興趣的可以查看的文檔。
CommonJS 定義了 module、exports 和 require 模塊規(guī)范,Node.js 為了實現(xiàn)這個簡單的標準,從底層 C/C++ 內(nèi)建模塊到 JavaScript 核心模塊,從路徑分析、文件定位到編譯執(zhí)行,經(jīng)歷了一系列復(fù)雜的過程。簡單的了解 Node 模塊的原理,有利于我們重新認識基于 Node 搭建的框架。一、CommonJS 模塊規(guī)范
CommonJS 規(guī)范或標準簡單來說是一種理論,它期望 JavaScript 可以具備跨宿主環(huán)境執(zhí)行的能力,不僅可以開發(fā)客戶端應(yīng)用,還可以開發(fā)服務(wù)端應(yīng)用、命令行工具、桌面圖形界面應(yīng)用等。
CommonJS 規(guī)范對模塊的定義分為三個部分:
模塊定義
在模塊中存在module對象代表模塊本身,模塊上下文提供exports屬性 ,將方法掛載在exports對象上即可以定義導(dǎo)出方式,例如:
// math.js exports.add = function(){ //...}
模塊引用
module提供require()方法引入外部模塊的 API 到當前的上下文中:
var math = require("math")
模塊標識
模塊標識實際就是傳遞給require()方法中的參數(shù),可以是按小駝峰(camelCase)命名的字符串,也可以是文件路徑。
曾經(jīng)困惑過,每個模塊都可以使用exports的情況下,為什么還必須用module.exports?
因為exports只是module.exports的一個地址引用,修改了exports的引用(如指向一個對象)就不能作為模塊導(dǎo)出。并且當module.exports已經(jīng)具備一些屬性和方法時,Node 會忽略exports只導(dǎo)出module.exports。所以直接賦值給module.exports會更準確。
Node.js 借鑒了 CommonJS 規(guī)范的設(shè)計,特別是 CommonJS 的 Modules 規(guī)范,實現(xiàn)了一套模塊系統(tǒng),同時 NPM 實現(xiàn)了 CommonJS 的 Packages 規(guī)范,模塊和包組成了 Node 應(yīng)用開發(fā)的基礎(chǔ)。二、Node 模塊加載原理
上述模塊規(guī)范看起來十分簡單,只有module、exports和require,但 Node 是如何實現(xiàn)的呢?
需要經(jīng)歷路徑分析(模塊的完整路徑)、文件定位(文件擴展名或目錄)、編譯執(zhí)行三個步驟。
2.1 路徑分析回顧require()接收 模塊標識 作為參數(shù)來引入模塊,Node 就是基于這個標識符進行路徑分析。不同的標識符采用的分析方式是不同的,主要分為一下幾類:
Node 提供的核心模塊,如 http、fs、path
核心模塊在 Node 源碼編譯時存為二進制執(zhí)行文件,在 Node 啟動時直接加載到內(nèi)存中,路徑分析中優(yōu)先判斷,所以加載速度很快,而且也不用后續(xù)的文件定位和編譯執(zhí)行。
如果想加載與核心模塊同名的自定義模塊,如自定義 http 模塊,那必須選用不同標志符或改用路徑方式。
路徑形式的文件模塊,.、..相對路徑模塊和/絕對路徑模塊
以.、..或/開始的標識符都會當成文件模塊處理,Node 會將require()中的路徑轉(zhuǎn)為真實路徑作為索引,然后編譯執(zhí)行。
由于文件模塊明確了文件位置,所以縮短了路徑分析時間,加載速度僅慢與核心模塊。
自定義模塊,即非路徑形式的文件模塊
即不是核心模塊,也不是路徑形式的文件模塊,自定義文件是特殊的文件模塊,在路徑查找時 Node 會逐級查找該模塊路徑中的路徑。
模塊路徑查找策略示例如下:
// paths.js console.log(module.paths) // Terminal $ node paths.js [ "/Users/tong/WebstormProjects/testNode/node_modules", "/Users/tong/WebstormProjects/node_modules", "/Users/tong/node_modules", "/Users/node_modules", "/node_modules" ]
從上述示例輸出的模塊路徑數(shù)組可以看出,模塊的查找時沿當前路徑向上逐級查找node_modules目錄,直到目標路徑為止,類似 JS 原型鏈或作用域鏈。路徑越深速度越慢,所以自定義模塊加載速度最慢。
緩存優(yōu)先機制:Node 會對引入過的模塊進行緩存以提高性能,不同于瀏覽器緩存的是文件,Node 緩存的是編譯和執(zhí)行后的對象,所以require()對相同模塊的二次加載采用緩存優(yōu)先的方式。這個緩存優(yōu)先是第一優(yōu)先級的,比核心模塊的優(yōu)先級要高!2.2 文件定位
模塊路徑分析完成后是文件定位,主要包括文件擴展名的分析、目錄和包的處理。為了表達的更清晰,將文件定位分為四個步驟:
step1: 補充擴展名
通常require()中的標識符是不包含文件擴展名的,這種情況下,Node會按照 .js、.json、.node 的順序嘗試補充擴展名。
在嘗試補充擴展名時,需要調(diào)用 fs 模塊同步阻塞式判斷文件是否存在,所以這里提升性能的小技巧,就是 .json 和 .node 文件傳遞給require()時帶上擴展名會加快一些速度。
step2: 目錄處理查找 pakage.json
如果補充擴展名后沒有找到對應(yīng)文件,但是得到了一個目錄,此時 Node會將目錄當做一個包處理。依據(jù) CommonJS 包規(guī)范的實現(xiàn),Node 會在目錄下查找pakage.json(包描述文件),通過JSON.parse()解析成包描述對象,從中取main屬性指定的文件名定位。
step3: 繼續(xù)默認查找 index 文件
如果沒有pakage.json或者main屬性指定的文件名錯誤,那 Node 會將 index 當做默認文件名,依次查找 index.js、index.json、index.node
step4: 進入下一個模塊路徑
在上述目錄分析過程中沒有成功定位時,自定義模塊按路徑查找策略進入上一層node_modules目錄,當整個模塊路徑數(shù)組遍歷完畢后沒有定位到文件,則會拋出查找失敗異常。
緩存加載的優(yōu)化策略使得二次引入不需要路徑分析、文件定位、編譯執(zhí)行這些過程,而且核心模塊也不需要文件定位的過程,這大大提高了再次加載模塊時的效率2.3 編譯執(zhí)行
Node 中每個模塊都是一個對象,在具體定位到文件后,Node 會新建該模塊對象,然后根據(jù)路徑載入并編譯。不同的文件擴展名載入方法為:
.js 文件: 通過 fs 模塊同步讀取后編譯執(zhí)行
.json 文件: 通過 fs 模塊同步讀取后,用JSON.parse()解析并返回結(jié)果
.node 文件: 這是用 C/C++ 寫的擴展文件,通過process.dlopen()方法加載最后編譯生成的
其他擴展名: 都被當做 js 文件載入
載入成功后 Node 會調(diào)用具體的編譯方式將文件執(zhí)行后返回給調(diào)用者。對于 .json 文件的編譯最簡單,JSON.parse()解析得到對象后直接賦值給模塊對象的exports,而 .node 文件是C/C++編譯生成的,Node 直接調(diào)用process.dlopen()載入執(zhí)行就可以,下面重點介紹 .js 文件的編譯:
在 CommonJS 模塊規(guī)范中有module、exports 和 require 這3個變量,在 Node API 文檔中每個模塊還有 __filename、__dirname這兩個變量,但是在模塊中沒有定義這些變量,那它們是怎么產(chǎn)生的呢?
事實上在編譯過程中,Node 對每個 JS 文件都被進行了封裝,每個文件都是一個模塊,有自己的作用域。例如一個 JS 文件會被封裝成如下:
(function (exports, require, module, __filename, __dirname) { var math = require("math") export.add = function(){ //... } })
首先每個模塊文件之間都進行了作用域隔離,通過vm原生模塊的runInThisContext()方法(類似 eval)返回一個具體的 function 對象,最后將當前模塊對象的exports屬性、require()方法、模塊對象本身module、文件定位時得到的完整路徑__filename和文件目錄__dirname作為參數(shù)傳遞給這個 function 執(zhí)行。模塊的exports屬性上的任何方法和屬性都可以被外部調(diào)用,其余的則不可被調(diào)用。
至此,module、exports 和 require的流程就介紹完了。
編譯成功的模塊會將文件路徑作為索引緩存在 Module._cache 對象上,路徑分析時優(yōu)先查找緩存,提高二次引入的性能。三、Node 核心模塊
總結(jié)來說 Node 模塊分為Node提供的核心模塊和用戶編寫的文件模塊。文件模塊是在運行時動態(tài)加載,包括了上述完整的路徑分析、文件定位、編譯執(zhí)行這些過程,核心模塊在Node源碼編譯成可執(zhí)行文件時存為二進制文件,直接加載在內(nèi)存中,所以不用文件定位和編譯執(zhí)行。
核心模塊分為 C/C++ 編寫的和 JavaScript 編寫的兩部分,在編譯所有 C/C++ 文件之前,編譯程序需要將所有的 JavaScript 核心模塊編譯為 C/C++ 可執(zhí)行代碼,編譯成功的則放在 NativeModule._cache對象上,顯然和文件模塊 Module._cache的緩存位置不同。
在核心模塊中,有些模塊由純 C/C++ 編寫的內(nèi)建模塊,主要提供 API 給 JavaScript 核心模塊,通常不能被用戶直接調(diào)用,而有些模塊由 C/C++ 完成核心部分,而 JavaScript 實現(xiàn)封裝和向外導(dǎo)出,如 buffer、fs、os 等。
所以在Node的模塊類型中存在依賴層級關(guān)系:內(nèi)建模塊(C/C++)—> 核心模塊(JavaScript)—> 文件模塊。
使用require()十分的方便,但從 JavaScript 到 C/C++ 的過程十分復(fù)雜,總結(jié)來說需要經(jīng)歷 C/C++ 層面內(nèi)建模塊的定義、(JavaScript)核心模塊的定義和引入以及(JavaScript)文件模塊的引入。
四、前端模塊規(guī)范對比前后端的 JavaScript,瀏覽器端的 JavaScript 需要經(jīng)歷從同一個服務(wù)器端分發(fā)到多個客戶端執(zhí)行,通過網(wǎng)絡(luò)加載代碼,瓶頸在于寬帶;而服務(wù)器端 JavaScript 相同代碼需要多次執(zhí)行,通過磁盤加載,瓶頸在于 CPU 和內(nèi)存,所以前后端的 JavaScript 在 Http 兩端的職責(zé)完全不用。
Node 模塊的引入幾乎是同步的,而前端模塊如果同步引入,那腳本加載需要太長的時間,所以 CommonJS 為后端 JavaScript 制定的規(guī)范不適合前端。而后出現(xiàn) AMD 和 CMD 用于前端應(yīng)用場景。
4.1 AMD 規(guī)范AMD 即異步模塊定義(Asynchronous Module Definition),模塊定義為:
define(id?, dependencies?, factory);
AMD 模塊需要用define明確定義一個模塊,其中模塊id與依賴dependencies是可選的,factory的內(nèi)容就是實際代碼的內(nèi)容。例如指定一些依賴到模塊中:
define(["dep1", "dep2"], function(){ // module code });
require.js 實現(xiàn) AMD 規(guī)范的模塊化,感興趣的可以查看 require.js 的文檔。
4.2 CMD 規(guī)范CMD 模塊的定義更加簡單:
define(factory);
定義的模塊同 Node 模塊一樣是隱式包裝,在依賴部分支持動態(tài)引入,例如:
define(function(require, exports, module){ // module code });
require、exports、module通過形參傳遞給模塊,需要依賴模塊時直接使用require()引入。
sea.js 實現(xiàn) AMD 規(guī)范的模塊化,感興趣的可以查看 sea.js 的文檔。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/102499.html
摘要:解決思路服務(wù)器端渲染服務(wù)器端和前端公用同一個應(yīng)用,然后通過構(gòu)建工具及配置,確定哪些組件需要再服務(wù)器端渲染,那些組件需要再客戶端渲染。服務(wù)器端渲染,由框架與構(gòu)建工具配合,并依據(jù)一定的項目結(jié)構(gòu)和編碼方式,共同運行。 分離 為什么需要 前后端分離、web服務(wù)器與static服務(wù)器分離: 前端與后端耦合 (需求) 自動化、工程化的構(gòu)建前端的代碼 (基礎(chǔ)條件) 模塊化、組件化,項目共享代碼 (...
摘要:什么是單頁面應(yīng)用單頁面應(yīng)用是指用戶在瀏覽器加載單一的頁面,后續(xù)請求都無需再離開此頁目標旨在用為用戶提供了更接近本地移動或桌面應(yīng)用程序的體驗。流程第一次請求時,將導(dǎo)航頁傳輸?shù)娇蛻舳?,其余請求通過獲取數(shù)據(jù)實現(xiàn)數(shù)據(jù)的傳輸通過或遠程過程調(diào)用。 什么是單頁面應(yīng)用(SPA)? 單頁面應(yīng)用(SPA)是指用戶在瀏覽器加載單一的HTML頁面,后續(xù)請求都無需再離開此頁 目標:旨在用為用戶提供了更接近本地...
摘要:什么是單頁面應(yīng)用單頁面應(yīng)用是指用戶在瀏覽器加載單一的頁面,后續(xù)請求都無需再離開此頁目標旨在用為用戶提供了更接近本地移動或桌面應(yīng)用程序的體驗。流程第一次請求時,將導(dǎo)航頁傳輸?shù)娇蛻舳?,其余請求通過獲取數(shù)據(jù)實現(xiàn)數(shù)據(jù)的傳輸通過或遠程過程調(diào)用。 什么是單頁面應(yīng)用(SPA)? 單頁面應(yīng)用(SPA)是指用戶在瀏覽器加載單一的HTML頁面,后續(xù)請求都無需再離開此頁 目標:旨在用為用戶提供了更接近本地...
摘要:前端個靈魂拷問,徹底搞明白你就是中級前端工程師上篇感覺大家比較喜歡看這種類型的文章,以后會多一些。所有依賴這個模塊的語句,都定義在一個回調(diào)函數(shù)中,等到加載完成之后,這個回調(diào)函數(shù)才會運行。此規(guī)范其實是在推廣過程中產(chǎn)生的。 showImg(https://segmentfault.com/img/bVbwAMU?w=700&h=394); 前端20個靈魂拷問,徹底搞明白你就是中級前端工程師...
閱讀 2795·2023-04-26 01:47
閱讀 3601·2023-04-25 23:45
閱讀 2481·2021-10-13 09:39
閱讀 617·2021-10-09 09:44
閱讀 1807·2021-09-22 15:59
閱讀 2786·2021-09-13 10:33
閱讀 1733·2021-09-03 10:30
閱讀 667·2019-08-30 15:53