node的模塊機制

?

Node.js模塊的實現

之前在網上查閱了許多介紹Node.js的文章,可惜對于Node.js的模塊機制大都著墨不多。在后續介紹模塊的使用之前,我認為有必要深入一下Node.js的模塊機制。

CommonJS規范

早在Netscape誕生不久后,JavaScript就一直在探索本地編程的路,Rhino是其代表產物。無奈那時服務端JavaScript走的路均是參考眾多服務器端語言來實現的,在這樣的背景之下,一沒有特色,二沒有實用價值。但是隨著JavaScript在前端的應用越來越廣泛,以及服務端JavaScript的推動,JavaScript現有的規范十分薄弱,不利于JavaScript大規模的應用。那些以JavaScript為宿主語言的環境中,只有本身的基礎原生對象和類型,更多的對象和API都取決于宿主的提供,所以,我們可以看到JavaScript缺少這些功能:

  • JavaScript沒有模塊系統。沒有原生的支持密閉作用域或依賴管理。
  • JavaScript沒有標準庫。除了一些核心庫外,沒有文件系統的API,沒有IO流API等。
  • JavaScript沒有標準接口。沒有如Web Server或者數據庫的統一接口。
  • JavaScript沒有包管理系統。不能自動加載和安裝依賴

于是便有了CommonJS(http://www.commonjs.org)規范的出現,其目標是為了構建JavaScript在包括Web服務器,桌面,命令行工具,及瀏覽器方面的生態系統。

CommonJS制定了解決這些問題的一些規范,而Node.js就是這些規范的一種實現。Node.js自身實現了require方法作為其引入模塊的方法,同時NPM也基于CommonJS定義的包規范,實現了依賴管理和模塊自動安裝等功能。這里我們將深入一下Node.js的require機制和NPM基于包規范的應用。

簡單模塊定義和使用

在Node.js中,定義一個模塊十分方便。我們以計算圓形的面積和周長兩個方法為例,來表現Node.js中模塊的定義方式。

var PI = Math.PI;
exports.area = function (r) {return PI * r * r;
};
exports.circumference = function (r) {return 2 * PI * r;
};

將這個文件存為circle.js,并新建一個app.js文件,并寫入以下代碼:

var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));

可以看到模塊調用也十分方便,只需要require需要調用的文件即可。

在require了這個文件之后,定義在exports對象上的方法便可以隨意調用。Node.js將模塊的定義和調用都封裝得極其簡單方便,從API對用戶友好這一個角度來說,Node.js的模塊機制是非常優秀的。

模塊載入策略

Node.js的模塊分為兩類,一類為原生(核心)模塊,一類為文件模塊。原生模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。另一類文件模塊是動態加載的,加載速度比原生模塊慢。但是Node.js對原生模塊和文件模塊都進行了緩存,于是在第二次require時,是不會有重復開銷的。其中原生模塊都被定義在lib這個目錄下面,文件模塊則不定性。

node app.js

由于通過命令行加載啟動的文件幾乎都為文件模塊。我們從Node.js如何加載文件模塊開始談起。加載文件模塊的工作,主要由原生模塊module來實現和完成,該原生模塊在啟動時已經被加載,進程直接調用到runMain靜態方法。

// bootstrap main module.
Module.runMain = function () {// Load the main module--the command line argument.Module._load(process.argv[1], null, true);
};

_load靜態方法在分析文件名之后執行

var module = new Module(id, parent);

并根據文件路徑緩存當前模塊對象,該模塊實例對象則根據文件名加載。

module.load(filename);

實際上在文件模塊中,又分為3類模塊。這三類文件模塊以后綴來區分,Node.js會根據后綴名來決定加載方法。

  • .js。通過fs模塊同步讀取js文件并編譯執行。
  • .node。通過C/C++進行編寫的Addon。通過dlopen方法進行加載。
  • .json。讀取文件,調用JSON.parse解析加載。

這里我們將詳細描述js后綴的編譯過程。Node.js在編譯js文件的過程中實際完成的步驟有對js文件內容進行頭尾包裝。以app.js為例,包裝之后的app.js將會變成以下形式:

(function (exports, require, module, __filename, __dirname) {var circle = require('./circle.js');console.log('The area of a circle of radius 4 is ' + circle.area(4));
});

這段代碼會通過vm原生模塊的runInThisContext方法執行(類似eval,只是具有明確上下文,不污染全局),返回為一個具體的function對象。最后傳入module對象的exports,require方法,module,文件名,目錄名作為實參并執行。

這就是為什么require并沒有定義在app.js 文件中,但是這個方法卻存在的原因。從Node.js的API文檔中可以看到還有__filename、__dirname、module、exports幾個沒有定義但是卻存在的變量。其中__filename和__dirname在查找文件路徑的過程中分析得到后傳入的。module變量是這個模塊對象自身,exports是在module的構造函數中初始化的一個空對象({},而不是null)。

在這個主文件中,可以通過require方法去引入其余的模塊。而其實這個require方法實際調用的就是load方法。

load方法在載入、編譯、緩存了module后,返回module的exports對象。這就是circle.js文件中只有定義在exports對象上的方法才能被外部調用的原因。

以上所描述的模塊載入機制均定義在lib/module.js中。

require方法中的文件查找策略

由于Node.js中存在4類模塊(原生模塊和3種文件模塊),盡管require方法極其簡單,但是內部的加載卻是十分復雜的,其加載優先級也各自不同。

從文件模塊緩存中加載

盡管原生模塊與文件模塊的優先級不同,但是都不會優先于從文件模塊的緩存中加載已經存在的模塊。

從原生模塊加載

原生模塊的優先級僅次于文件模塊緩存的優先級。require方法在解析文件名之后,優先檢查模塊是否在原生模塊列表中。以http模塊為例,盡管在目錄下存在一個http/http.js/http.node/http.json文件,require(“http”)都不會從這些文件中加載,而是從原生模塊中加載。

原生模塊也有一個緩存區,同樣也是優先從緩存區加載。如果緩存區沒有被加載過,則調用原生模塊的加載方式進行加載和執行。

從文件加載

當文件模塊緩存中不存在,而且不是原生模塊的時候,Node.js會解析require方法傳入的參數,并從文件系統中加載實際的文件,加載過程中的包裝和編譯細節在前一節中已經介紹過,這里我們將詳細描述查找文件模塊的過程,其中,也有一些細節值得知曉。

require方法接受以下幾種參數的傳遞:

  • http、fs、path等,原生模塊。
  • ./mod或../mod,相對路徑的文件模塊。
  • /pathtomodule/mod,絕對路徑的文件模塊。
  • mod,非原生模塊的文件模塊。

在進入路徑查找之前有必要描述一下module path這個Node.js中的概念。對于每一個被加載的文件模塊,創建這個模塊對象的時候,這個模塊便會有一個paths屬性,其值根據當前文件的路徑計算得到。我們創建modulepath.js這樣一個文件,其內容為:

console.log(module.paths);

我們將其放到任意一個目錄中執行node modulepath.js命令,將得到以下的輸出結果。

[ '/home/jackson/research/node_modules',
'/home/jackson/node_modules',
'/home/node_modules',
'/node_modules' ]

Windows下:

[ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ]

可以看出module path的生成規則為:從當前文件目錄開始查找node_modules目錄;然后依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄。

除此之外還有一個全局module path,是當前node執行文件的相對目錄(../../lib/node)。如果在環境變量中設置了HOME目錄和NODE_PATH目錄的話,整個路徑還包含NODE_PATH和HOME目錄下的.node_libraries與.node_modules。其最終值大致如下:

[NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node]

下圖是筆者從源代碼中整理出來的整個文件查找流程:

簡而言之,如果require絕對路徑的文件,查找時不會去遍歷每一個node_modules目錄,其速度最快。其余流程如下:

  1. 從module path數組中取出第一個目錄作為查找基準。
  2. 直接從目錄中查找該文件,如果存在,則結束查找。如果不存在,則進行下一條查找。
  3. 嘗試添加.js、.json、.node后綴后查找,如果存在文件,則結束查找。如果不存在,則進行下一條。
  4. 嘗試將require的參數作為一個包來進行查找,讀取目錄下的package.json文件,取得main參數指定的文件。
  5. 嘗試查找該文件,如果存在,則結束查找。如果不存在,則進行第3條查找。
  6. 如果繼續失敗,則取出module path數組中的下一個目錄作為基準查找,循環第1至5個步驟。
  7. 如果繼續失敗,循環第1至6個步驟,直到module path中的最后一個值。
  8. 如果仍然失敗,則拋出異常。

整個查找過程十分類似原型鏈的查找和作用域的查找。所幸Node.js對路徑查找實現了緩存機制,否則由于每次判斷路徑都是同步阻塞式進行,會導致嚴重的性能消耗。

包結構

前面提到,JavaScript缺少包結構。CommonJS致力于改變這種現狀,于是定義了包的結構規范(http://wiki.commonjs.org/wiki/Packages/1.0?)。而NPM的出現則是為了在CommonJS規范的基礎上,實現解決包的安裝卸載,依賴管理,版本管理等問題。require的查找機制明了之后,我們來看一下包的細節。

一個符合CommonJS規范的包應該是如下這種結構:

  • 一個package.json文件應該存在于包頂級目錄下
  • 二進制文件應該包含在bin目錄下。
  • JavaScript代碼應該包含在lib目錄下。
  • 文檔應該在doc目錄下。
  • 單元測試應該在test目錄下。

由上文的require的查找過程可以知道,Node.js在沒有找到目標文件時,會將當前目錄當作一個包來嘗試加載,所以在package.json文件中最重要的一個字段就是main。而實際上,這一處是Node.js的擴展,標準定義中并不包含此字段,對于require,只需要main屬性即可。但是在除此之外包需要接受安裝、卸載、依賴管理,版本管理等流程,所以CommonJS為package.json文件定義了如下一些必須的字段:

    • name。包名,需要在NPM上是唯一的。不能帶有空格。
    • description。包簡介。通常會顯示在一些列表中。
    • version。版本號。一個語義化的版本號(http://semver.org/?),通常為x.y.z。該版本號十分重要,常常用于一些版本控制的場合。
    • keywords。關鍵字數組。用于NPM中的分類搜索。
    • maintainers。包維護者的數組。數組元素是一個包含name、email、web三個屬性的JSON對象。
    • contributors。包貢獻者的數組。第一個就是包的作者本人。在開源社區,如果提交的patch被merge進master分支的話,就應當加上這個貢獻patch的人。格式包含name和email。如:
"contributors": [{"name": "Jackson Tian","email": "mail @gmail.com"}, {"name": "fengmk2","email": "mail2@gmail.com"
}],
    • bugs。一個可以提交bug的URL地址。可以是郵件地址(mailto:mailxx@domain),也可以是網頁地址(http://url)。
    • licenses。包所使用的許可證。例如:
"licenses": [{"type": "GPLv2","url": "http://www.example.com/licenses/gpl.html",
}]
  • repositories。托管源代碼的地址數組。
  • dependencies。當前包需要的依賴。這個屬性十分重要,NPM會通過這個屬性,幫你自動加載依賴的包。

以下是Express框架的package.json文件,值得參考。

{"name": "express","description": "Sinatra inspired web development framework","version": "3.0.0alpha1-pre","author": "TJ Holowaychuk 

除了前面提到的幾個必選字段外,我們還發現了一些額外的字段,如bin、scripts、engines、devDependencies、author。這里可以重點提及一下scripts字段。包管理器(NPM)在對包進行安裝或者卸載的時候需要進行一些編譯或者清除的工作,scripts字段的對象指明了在進行操作時運行哪個文件,或者執行拿條命令。如下為一個較全面的scripts案例:

"scripts": {"install": "install.js","uninstall": "uninstall.js","build": "build.js","doc": "make-doc.js","test": "test.js",
}

如果你完善了自己的JavaScript庫,使之實現了CommonJS的包規范,那么你可以通過NPM來發布自己的包,為NPM上5000+的基礎上再加一個模塊。

npm publish <folder>

命令十分簡單。但是在這之前你需要通過npm adduser命令在NPM上注冊一個帳戶,以便后續包的維護。NPM會分析該文件夾下的package.json文件,然后上傳目錄到NPM的站點上。用戶在使用你的包時,也十分簡明:

npm install <package>

甚至對于NPM無法安裝的包(因為某些奇怪的網絡原因),可以通過github手動下載其穩定版本,解壓之后通過以下命令進行安裝:

npm install <package.json folder>

只需將路徑指向package.json存在的目錄即可。然后在代碼中require('package')即可使用。

Node.js中的require內部流程之復雜,而方法調用之簡單,實在值得嘆為觀止。更多NPM使用技巧可以參見http://www.infoq.com/cn/articles/msh-using-npm-manage-node.js-dependence。

Node.js模塊與前端模塊的異同

通常有一些模塊可以同時適用于前后端,但是在瀏覽器端通過script標簽的載入JavaScript文件的方式與Node.js不同。Node.js在載入到最終的執行中,進行了包裝,使得每個文件中的變量天然的形成在一個閉包之中,不會污染全局變量。而瀏覽器端則通常是裸露的JavaScript代碼片段。所以為了解決前后端一致性的問題,類庫開發者需要將類庫代碼包裝在一個閉包內。以下代碼片段抽取自著名類庫underscore的定義方式。

(function () {// Establish the root object, `window` in the browser, or `global` on the server.var root = this;var _ = function (obj) {return new wrapper(obj);};if (typeof exports !== 'undefined') {if (typeof module !== 'undefined' && module.exports) {exports = module.exports = _;}exports._ = _;} else if (typeof define === 'function' && define.amd) {// Register as a named module with AMD.define('underscore', function () {return _;});} else {root['_'] = _;}
}).call(this);

首先,它通過function定義構建了一個閉包,將this作為上下文對象直接call調用,以避免內部變量污染到全局作用域。續而通過判斷exports是否存在來決定將局部變量_綁定給exports,并且根據define變量是否存在,作為處理在實現了AMD規范環境(http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition)下的使用案例。僅只當處于瀏覽器的環境中的時候,this指向的是全局對象(window對象),才將_變量賦在全局對象上,作為一個全局對象的方法導出,以供外部調用。

所以在設計前后端通用的JavaScript類庫時,都有著以下類似的判斷:

if (typeof exports !== "undefined") {exports.EventProxy = EventProxy;
} else {this.EventProxy = EventProxy;
}

即,如果exports對象存在,則將局部變量掛載在exports對象上,如果不存在,則掛載在全局對象上。

轉載于:https://www.cnblogs.com/xiongmaoblog/p/7284264.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/282871.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/282871.shtml
英文地址,請注明出處:http://en.pswp.cn/news/282871.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

vs使用ado連接oracle,在VS環境下以ADO方式操作Oracle數據庫

利用ADO引擎方式訪問Oracle數據庫的實現方法&#xff1a;定義數據庫頭文件為CDBOperation.h#pragma once#import "C:\Program Files\Common Files\System\ADO\msado15.dll" no_namespace rename("EOF","adoEOF"),rename("LockTypeEnum"…

httpstat:一個檢查網站性能的 curl 統計分析工具

httpstat&#xff1a;一個檢查網站性能的 curl 統計分析工具httpstat 是一個 Python 腳本&#xff0c;它以美妙妥善的方式反映了 curl 統計分析&#xff0c;它是一個單一腳本&#xff0c;兼容 Python 3 &#xff0c;在用戶的系統上不需要安裝額外的軟件(依賴)。作者&#xff1a…

Unity(創建腳本)

#一、描述 記錄第一課時&#xff0c;腳本的創建與使用基本的API #二、學習記錄 &#xff08;一&#xff09;創建一個Cube方塊 &#xff08;二&#xff09;在cube組件上添加一個腳本&#xff0c;選中cube組件&#xff0c;在屏幕右側有著cube的組件屬性欄&#xff0c;點擊AddComp…

關于面試中看到一些問題

最近公司在招聘.NET開發人員&#xff0c;面試了一些人&#xff0c;有一些感悟&#xff0c;分享出來&#xff0c;以供參考。面試的人員中&#xff0c;有一些是三五年的開發人員&#xff1b;也有幾個是10年左右的技術負責人&#xff0c;不但自己架構過項目&#xff0c;還有帶領導…

jQuery遍歷not的用法

從包含所有段落的集合中刪除 id 為 "selected" 的段落&#xff1a; $("p").not("#selected") 定義和用法 not() 從匹配元素集合中刪除元素。 語法 1 .not(selector) 參數描述selector字符串值&#xff0c;包含用于匹配元素的選擇器表達式。語法 …

linux 字符串加入中括號,Linux Shell 基礎 -- 總結幾種括號、引號的用法

1、雙引號 " "雙引號常用于包含一組字符串&#xff0c;在雙引號中&#xff0c;除了 "$"、""、" (反引號)"有特殊含義外&#xff0c;其余字符(如IFS、換行符、回車符等)沒有特殊含義。$ a3$ echo "$a"輸出結果為 3&#xff…

設計模式相關

多例模式 轉載于:https://www.cnblogs.com/our880tom/p/6392983.html

一個countDown在多線程調度下使用不當的分享

2019獨角獸企業重金招聘Python工程師標準>>> 一個countDown在多線程調度下使用不當的分享 1. 詭異的數據抖動 在一個需求開發過程中&#xff0c;由于有多角色需要獲取每個角色下的菜單&#xff1b;結果出現了單角色下拉去菜單沒問題&#xff0c;多角色情況下只有一個…

我堅持三年了!

閱讀本文大概需要5分鐘。不知不覺&#xff0c;公眾號寫作已經持續了3年了。2019年11月底&#xff0c;心血來潮寫了第一篇文章&#xff0c;更多是為了復盤過去的一些工作經歷。在前幾天&#xff0c;讀者數突破了16萬&#xff0c;雖然這個數字相比那些頭部大號而言并不多&#xf…

關于Qt模態框總匯

轉載請注明出處&#xff1a;http://www.cnblogs.com/dachen408/p/7285710.html 父窗體為QMainWindow&#xff1b; 當子窗體為&#xff1a; 1.QWidget&#xff0c;需要設置 this->setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); this->setWindowModality(Qt::Win…

linux腳本打印循環次數,shell腳本編程基礎(3)——循環用法

本節索引&#xff1a;一、if、case條件判斷二、for、while及until循環三、循環控制語句continue、break、shift及select菜單四、信號捕捉trap在前面的基礎編程內容中&#xff0c;我們已經學習了shell腳本的順序執行及選擇執行&#xff0c;通過這兩種方式&#xff0c;可以幫我們…

RTSP服務器之————rtsp-server(輕量級RTSP / RTP流媒體服務器)

github&#xff1a;https://github.com/revmischa/rtsp-server 輕量級RTSP / RTP流媒體服務器

EF CORE 7 中的新功能:使用 ExecuteDelete 和 ExecuteUpdate 進行批量操作

原文鏈接&#xff1a;https://timdeschryver.dev/blog/new-in-entity-framework-7-bulk-operations-with-executedelete-and-executeupdate原文作者&#xff1a;tim_deschryver翻譯&#xff1a;沙漠盡頭的狼(谷歌翻譯加持)Entity Framework 7 包括一些已被要求的流行功能&#…

java 簡單json和對象相互轉換

2019獨角獸企業重金招聘Python工程師標準>>> package Fasterxml; import com.fasterxml.jackson.databind.ObjectMapper; import mode.User; import java.io.StringWriter; import java.util.ArrayList; import java.util.List;/*** maven...**<dependency>* …

暢想動畫制作的樂趣

為什么要制作動畫&#xff1f; 現在的營銷活動&#xff0c;用一個很簡單的圖片去吸引消費者已經遠遠不夠。想讓消費者創造GMV&#xff0c;肯定需要讓消費者覺得眼前一亮或是有視覺沖擊的東西&#xff0c;或者在動畫過程中提供更好的引導部分&#xff0c;比如紅包&#xff0c;引…

Linux的scan命令,linux的scan命令

linux下scan命令主要是以scanf的形式使用轉換符解析字符串&#xff0c;下面由秋天網 Qiutian.ZqNF.Com小編為大家整理了linux下scan命令的相關知識&#xff0c;希望對大家有幫助!linux的scan命令詳解scan - 以sscanf的形式使用轉換符解析字符串語法:scan string format ?varna…

Spring Cloud Gateway 原生支持接口限流該怎么玩

關于pig&#xff1a; 基于Spring Cloud、oAuth2.0開發基于Vue前后分離的開發平臺&#xff0c;支持賬號、短信、SSO等多種登錄&#xff0c;提供配套視頻開發教程。 關于 Spring Cloud Gateway SpringCloudGateway是Spring官方基于Spring 5.0&#xff0c;Spring Boot 2.0和Projec…

我的手機 不支持箭頭函數

不支持&#xff0c;要換成function的形式 轉載于:https://www.cnblogs.com/web-fusheng/p/7295901.html

中標麒麟linux卸載qt,國產化 銀河麒麟編譯Qt程序的問題匯總 | 阿拉燈

Run in terminal莫名奇妙軟件無法在QtCreator中運行或者調試&#xff0c;main函數都無法進入&#xff0c;QtCreator中一運行就崩潰&#xff0c;并跳到匯編界面&#xff0c;這多半和代碼沒什么關系&#xff0c;我這里是將項目->運行中的“Run in terminal”去掉勾選&#xff…

css3-13 如何改變文本框的輪廓顏色

css3-13 如何改變文本框的輪廓顏色 一、總結 一句話總結&#xff1a;outline使用和border很像&#xff0c;幾乎一模一樣&#xff0c;多了一個offset屬性 1、輪廓outline如何使用&#xff1f; 使用和border很像&#xff0c;幾乎一模一樣&#xff0c;多了一個offset屬性 18 …