本篇文章帶給大家的內容是關於Node中模組實作過程的詳細介紹(附範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
CommonJS 定義了module、exports 和require 模組規範,Node.js 為了實現這個簡單的標準,從底層C/C 內建模區塊到JavaScript 核心模組,從路徑分析、檔案定位到編譯執行,經歷了一系列複雜的過程。簡單的了解 Node 模組的原理,有利於我們重新認識基於 Node 搭建的框架。
一、CommonJS 模組規格
CommonJS 規格或標準簡單來說是一種理論,它期望JavaScript 可以具備跨宿主環境執行的能力,不僅可以開發客戶端應用,還可以開發服務端應用程式、命令列工具、桌面圖形介面應用程式等。
CommonJS 規範對模組的定義分為三個部分:
模組定義
#在模組中存在module物件代表模組本身,模組上下文提供exports屬性,將方法掛載在exports物件上即可以定義導出方式,例如:
// math.js exports.add = function(){ //...}
模組引用
module提供require()方法引入外部模組的API 到目前的上下文:
var math = require('math')模組標識
模組標識實際上就是傳遞給require()方法中的參數,可以是按小駝峰(camelCase)命名的字串,也可以是檔案路徑。
Node.js 借鏡了 CommonJS 規範的設計,特別是 CommonJS 的 Modules 規範,實作了一套模組系統,同時 NPM 實作了 CommonJS 的 Packages 規範,模組和套件組成了 Node 應用開發的基礎。
二、Node 模組載入原理
上述模組規格看起來十分簡單,只有module、exports和require,但 Node 是如何實現的呢?
需要經歷路徑分析(模組的完整路徑)、檔案定位(檔案副檔名或目錄)、編譯執行三個步驟。
2.1 路徑分析
回顧require()接收 模組識別 作為參數來引入模組,Node 就是基於這個標識符進行路徑分析。不同的標識符所採用的分析方式是不同的,主要分為幾類:
Node 提供的核心模組,如http、fs、path
核心模組在Node 原始碼編譯時存為二進位執行文件,在Node 啟動時直接載入到記憶體中,路徑分析中優先判斷,所以載入速度很快,而且也不用後續的文件定位和編譯執行。
如果想要載入與核心模組同名的自訂模組,如自訂 http 模組,那必須選用不同標誌符號或改用路徑方式。
路徑形式的檔案模組,.、..相對路徑模組和/絕對路徑模組
以.、..或/開始的識別碼都會當成檔案模組處理,Node 會將require()中的路徑轉為真實路徑作為索引,然後編譯執行。
由於檔案模組明確了檔案位置,所以縮短了路徑分析時間,載入速度僅慢與核心模組。
自訂模組,即非路徑形式的檔案模組
即不是核心模組,也不是路徑形式的檔案模組,自訂檔案是特殊的檔案模組,在路徑尋找時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' ]
從上述範例輸出的模組路徑陣列可以看出,模組的尋找時沿著目前路徑向上逐級尋找node_modules目錄,直到目標路徑為止,類似JS原型鍊或作用域鏈。路徑越深速度越慢,所以自訂模組載入速度最慢。
快取優先機制:Node 會對引入的模組進行快取以提高效能,不同於瀏覽器快取的是文件,Node 快取的是編譯和執行後的對象,所以require()對相同模組的二次載入採用快取優先的方式。這個快取優先是第一優先權的,比核心模組的優先權要高!
2.2 檔案定位
模組路徑分析完成後是檔案定位,主要包括檔案副檔名的分析、目錄和套件的處理。為了表達的更清晰,將檔案定位分為四個步驟:
step1: 補充副檔名
#通常require()中的識別碼是不包含檔案副檔名的,這種情況下,Node會按照.js、.json、.node 的順序嘗試補充副檔名。
在嘗試補充副檔名時,需要呼叫fs 模組同步阻塞式判斷檔案是否存在,所以這裡提升效能的小技巧,就是.json 和.node 檔案傳遞給require()時帶上副檔名會加快一些速度。
step2: 目錄處理查找 pakage.json
如果補充副檔名後沒有找到對應文件,但是得到了一個目錄,此時 Node會將目錄當做一個包處理。依據 CommonJS 套件規範的實現,Node 會在目錄下尋找pakage.json(套件描述檔),透過JSON.parse()解析成套件描述對象,從中取main屬性指定的檔案名稱定位。
step3: 繼續預設找 index 檔案
如果没有pakage.json或者main属性指定的文件名错误,那 Node 会将 index 当做默认文件名,依次查找 index.js、index.json、index.node
step4: 进入下一个模块路径
在上述目录分析过程中没有成功定位时,自定义模块按路径查找策略进入上一层node_modules目录,当整个模块路径数组遍历完毕后没有定位到文件,则会抛出查找失败异常。
缓存加载的优化策略使得二次引入不需要路径分析、文件定位、编译执行这些过程,而且核心模块也不需要文件定位的过程,这大大提高了再次加载模块时的效率
2.3 编译执行
Node 中每个模块都是一个对象,在具体定位到文件后,Node 会新建该模块对象,然后根据路径载入并编译。不同的文件扩展名载入方法为:
.js 文件: 通过 fs 模块同步读取后编译执行.json 文件: 通过 fs 模块同步读取后,用JSON.parse()解析并返回结果.node 文件: 这是用 C/C++ 写的扩展文件,通过process.dlopen()方法加载最后编译生成的其他扩展名: 都被当做 js 文件载入
载入成功后 Node 会调用具体的编译方式将文件执行后返回给调用者。对于 .json 文件的编译最简单,JSON.parse()解析得到对象后直接赋值给模块对象的exports,而 .node 文件是C/C++编译生成的,Node 直接调用process.dlopen()载入执行就可以,下面重点介绍 .js 文件的编译:
在 CommonJS 模块规范中有module、exports 和 require 这3个变量,在 Node API 文档中每个模块还有 __filename、__dirname这两个变量,但是在模块中没有定义这些变量,那它们是怎么产生的呢?
事实上在编译过程中,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作为参数传递给这个 function 执行。模块的exports属性上的任何方法和属性都可以被外部调用,其余的则不可被调用。
至此,module、exports 和 require的流程就介绍完了。
曾经困惑过,每个模块都可以使用exports的情况下,为什么还必须用module.exports。
这是因为exports在编译过程中时通过形参传入的,直接给exports形参赋值只改变形参的引用,不能改变作用域外的值,例如:
let change = function (exports) { exports = 100 console.log(exports) } var exports = 2 change(exports) // 100 console.log(exports) // 2
所以直接赋值给module.exports对象就不会改变形参的引用了。
编译成功的模块会将文件路径作为索引缓存在 Module._cache 对象上,路径分析时优先查找缓存,提高二次引入的性能。
三、Node 核心模块
总结来说 Node 模块分为Node提供的核心模块和用户编写的文件模块。文件模块是在运行时动态加载,包括了上述完整的路径分析、文件定位、编译执行这些过程,核心模块在Node源码编译成可执行文件时存为二进制文件,直接加载在内存中,所以不用文件定位和编译执行。
核心模块分为 C/C++ 编写的和 JavaScript 编写的两部分,在编译所有 C/C++ 文件之前,编译程序需要将所有的 JavaScript 核心模块编译为 C/C++ 可执行代码,编译成功的则放在 NativeModule._cache对象上,显然和文件模块 Module._cache的缓存位置不同。
在核心模块中,有些模块由纯 C/C++ 编写的内建模块,主要提供 API 给 JavaScript 核心模块,通常不能被用户直接调用,而有些模块由 C/C++ 完成核心部分,而 JavaScript 实现封装和向外导出,如 buffer、fs、os 等。
所以在Node的模块类型中存在依赖层级关系:内建模块(C/C++)—> 核心模块(JavaScript)—> 文件模块。
使用require()十分的方便,但从 JavaScript 到 C/C++ 的过程十分复杂,总结来说需要经历 C/C++ 层面内建模块的定义、(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块的引入。
四、前端模块规范
对比前后端的 JavaScript,浏览器端的 JavaScript 需要经历从同一个服务器端分发到多个客户端执行,通过网络加载代码,瓶颈在于宽带;而服务器端 JavaScript 相同代码需要多次执行,通过磁盘加载,瓶颈在于 CPU 和内存,所以前后端的 JavaScript 在 Http 两端的职责完全不用。
Node 模块的引入几乎是同步的,而前端模块如果同步引入,那脚本加载需要太长的时间,所以 CommonJS 为后端 JavaScript 制定的规范不适合前端。而后出现 AMD 和 CMD 用于前端应用场景。
4.1 AMD 规范
AMD 即异步模块定义(Asynchronous Module Definition),模块定义为:
define(id?, dependencies?, factory);
AMD 模块需要用define明确定义一个模块,其中模块id与依赖dependencies是可选的,factory的内容就是实际代码的内容。例如指定一些依赖到模块中:
define(['dep1', 'dep2'], function(){ // module code });
require.js 实现 AMD 规范的模块化,感兴趣的可以查看 require.js 的文档。
4.2 CMD 规范
CMD 模块的定义更加简单:
define(factory);
定义的模块同 Node 模块一样是隐式包装,在依赖部分支持动态引入,例如:
define(function(require, exports, module){ // module code });
require
、exports
、module
通过形参传递给模块,需要依赖模块时直接使用require()
引入。
sea.js 实现 AMD 规范的模块化,感兴趣的可以查看 sea.js 的文档。
以上是Node中模組實作過程的詳細介紹(附範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!