【轉】Commonjs規範及Node模組實現

來源:互聯網
上載者:User

標籤:接下來   執行   file   地址   for   索引   不同的   擷取   script   

  Node在實現中並非完全按照CommonJS規範實現,而是對模組規範進行了一定的取捨,同時也增加了少許自身需要的特性。本文將詳細介紹NodeJS的模組實現

 

引入

  nodejs是區別於javascript的,在javascript中的頂層對象是window,而在node中的頂層對象是global

  [注意]實際上,javascript也存在global對象,只是其並不對外訪問,而使用window對象指向global對象而已

  在javascript中,通過var a = 100;是可以通過window.a來得到100的

  但在nodejs中,是不能通過global.a來訪問,得到的是undefined

  這是因為var a = 100;這個語句中的變數a,只是模組範圍內的變數a,而不是global對象下的a

  在nodejs中,一個檔案就是一個模組,每個模組都有自己的範圍。使用var來聲明的一個變數,它並不是全域的,而是屬於當前模組下

  如果要在全域範圍下聲明變數,則如下所示

 

概述

  Node中模組分為兩類:一類是Node提供的模組,稱為核心模組;另一類是使用者編寫的模組,稱為檔案模組

  核心模組部分在Node原始碼的編譯過程中,編譯進了二進位執行檔案。在Node進程啟動時,部分核心模組就被直接載入進記憶體中,所以這部分核心模組引入時,檔案定位和編譯執行這兩個步驟可以省略掉,並且在路徑分析中優先判斷,所以它的載入速度是最快的

  檔案模組則是在運行時動態載入,需要完整的路徑分析、檔案定位、編譯執行過程,速度比核心模組慢

  接下來,我們展開詳細的模組載入過程

 

模組載入

  在javascript中,載入模組使用script標籤即可,而在nodejs中,如何在一個模組中,載入另一個模組呢?

  使用require()方法來引入

【緩衝載入】

  再展開介紹require()方法的標識符分析之前,需要知道,與前端瀏覽器會緩衝靜態指令檔以提高效能一樣,Node對引入過的模組都會進行緩衝,以減少二次引入時的開銷。不同的地方在於,瀏覽器僅僅快取檔案,而Node緩衝的是編譯和執行之後的對象

  不論是核心模組還是檔案模組,require()方法對相同模組的二次載入都一律採用緩衝優先的方式,這是第一優先順序的。不同之處在於核心模組的緩衝檢查先於檔案模組的緩衝檢查

【標識符分析】

  require()方法接受一個標識符作為參數。在Node實現中,正是基於這樣一個標識符進行模組尋找的。模組標識符在Node中主要分為以下幾類:[1]核心模組,如http、fs、path等;[2].或..開始的相對路徑檔案模組;[3]以/開始的絕對路徑檔案模組;[4]非路徑形式的檔案模組,如自訂的connect模組

  根據參數的不同格式,require命令去不同路徑尋找模組檔案

  1、如果參數字串以“/”開頭,則表示載入的是一個位於絕對路徑的模組檔案。比如,require(‘/home/marco/foo.js‘)將載入/home/marco/foo.js

  2、如果參數字串以“./”開頭,則表示載入的是一個位於相對路徑(跟當前執行指令碼的位置相比)的模組檔案。比如,require(‘./circle‘)將載入當前指令碼同一目錄的circle.js

  3、如果參數字串不以“./“或”/“開頭,則表示載入的是一個預設提供的核心模組(位於Node的系統安裝目錄中),或者一個位於各級node_modules目錄的已安裝模組(全域安裝或局部安裝)

  [注意]如果是當前路徑下的檔案模組,一定要以./開頭,否則nodejs會試圖去載入核心模組,或node_modules內的模組 

//a.jsconsole.log(‘aaa‘);//b.jsrequire(‘./a‘);//‘aaa‘require(‘a‘);//報錯

【副檔名分析】

  require()在分析標識符的過程中,會出現標識符中不包含副檔名的情況。CommonJS模組規範也允許在標識符中不包含副檔名,這種情況下,Node會先尋找是否存在沒有尾碼的該檔案,如果沒有,再按.js、.json、.node的次序補足副檔名,依次嘗試

  在嘗試的過程中,需要調用fs模組同步阻塞式地判斷檔案是否存在。因為Node是單線程的,所以這裡是一個會引起效能問題的地方。小訣竅是:如果是.node和.json檔案,在傳遞給require()的標識符中帶上副檔名,會加快一點速度。另一個訣竅是:同步配合緩衝,可以大幅度緩解Node單線程中阻塞式調用的缺陷

【目錄分析和包】

  在分析標識符的過程中,require()通過分析副檔名之後,可能沒有尋找到對應檔案,但卻得到一個目錄,這在引入自訂模組和逐個模組路徑進行尋找時經常會出現,此時Node會將目錄當做一個包來處理

  在這個過程中,Node對CommonJS包規範進行了一定程度的支援。首先,Node在目前的目錄下尋找package.json(CommonJS包規範定義的包描述檔案),通過JSON.parse()解析出包描述對象,從中取出main屬性指定的檔案名稱進行定位。如果檔案名稱缺少副檔名,將會進入副檔名分析的步驟

  而如果main屬性指定的檔案名稱錯誤,或者壓根沒有package.json檔案,Node會將index當做預設檔案名稱,然後依次尋找index.js、index.json、index.node

  如果在目錄分析的過程中沒有定位成功任何檔案,則自訂模組進入下一個模組路徑進行尋找。如果模組路徑數組都被遍曆完畢,依然沒有尋找到目標檔案,則會拋出尋找失敗的異常

 

訪問變數

  如何在一個模組中訪問另外一個模組中定義的變數呢? 

【global】

  最容易想到的方法,把一個模組定義的變數複製到全域環境global中,然後另一個模組訪問全域環境即可

//a.jsvar a = 100;global.a = a;//b.jsrequire(‘./a‘);console.log(global.a);//100

  這種方法雖然簡單,但由於會汙染全域環境,不推薦使用

【module】

  而常用的方法是使用nodejs提供的模組對象Module,該對象儲存了當前模組相關的一些資訊

function Module(id, parent) {    this.id = id;    this.exports = {};    this.parent = parent;    if (parent && parent.children) {        parent.children.push(this);    }    this.filename = null;    this.loaded = false;    this.children = [];}
module.id 模組的識別符,通常是帶有絕對路徑的模組檔案名稱。module.filename 模組的檔案名稱,帶有絕對路徑。module.loaded 返回一個布爾值,表示模組是否已經完成載入。module.parent 返回一個對象,表示調用該模組的模組。module.children 返回一個數組,表示該模組要用到的其他模組。module.exports 表示模組對外輸出的值。

【exports】

  module.exports屬性工作表示當前模組對外輸出的介面,其他檔案載入該模組,實際上就是讀取module.exports變數

//a.jsvar a = 100;module.exports.a = a;//b.jsvar result = require(‘./a‘);console.log(result);//‘{ a: 100 }‘

  為了方便,Node為每個模組提供一個exports變數,指向module.exports。造成的結果是,在對外輸出模組介面時,可以向exports對象添加方法

console.log(module.exports === exports);//true

  [注意]不能直接將exports變數指向一個值,因為這樣等於切斷了exportsmodule.exports的聯絡

 

模組編譯

  編譯和執行是模組實現的最後一個階段。定位到具體的檔案後,Node會建立一個模組對象,然後根據路徑載入並編譯。對於不同的副檔名,其載入方法也有所不同,具體如下所示

  js檔案——通過fs模組同步讀取檔案後編譯執行

  node檔案——這是用C/C++編寫的擴充檔案,通過dlopen()方法載入最後編譯產生的檔案

  json檔案——通過fs模組同步讀取檔案後,用JSON.parse()解析返回結果

  其餘副檔名檔案——它們都被當做.js檔案載入

  每一個編譯成功的模組都會將其檔案路徑作為索引緩衝在Module._cache對象上,以提高二次引入的效能

  根據不同的副檔名,Node會調用不同的讀取方式,如.json檔案的調用如下:

// Native extension for .jsonModule._extensions[‘.json‘] = function(module, filename) {    var content = NativeModule.require(‘fs‘).readFileSync(filename, ‘utf8‘);     try {        module.exports = JSON.parse(stripBOM(content));    } catch (err) {        err.message = filename + ‘: ‘ + err.message;        throw err;    }};

  其中,Module._extensions會被賦值給require()的extensions屬性,所以通過在代碼中訪問require.extensions可以知道系統中已有的擴充載入方式。編寫如下代碼測試一下:

console.log(require.extensions);

  得到的執行結果如下:

{ ‘.js‘: [Function], ‘.json‘: [Function], ‘.node‘: [Function] }

  在確定檔案的副檔名之後,Node將調用具體的編譯方式來將檔案執行後返回給調用者

【JavaScript模組的編譯】

  回到CommonJS模組規範,我們知道每個模組檔案中存在著require、exports、module這3個變數,但是它們在模組檔案中並沒有定義,那麼從何而來呢?甚至在Node的API文檔中,我們知道每個模組中還有filename、dirname這兩個變數的存在,它們又是從何而來的呢?如果我們把直接定義模組的過程放諸在瀏覽器端,會存在汙染全域變數的情況

  事實上,在編譯的過程中,Node對擷取的JavaScript檔案內容進行了頭尾封裝。在頭部添加了(function(exports, require, module, filename, dirname) {\n,在尾部添加了\n});

  一個正常的JavaScript檔案會被封裝成如下的樣子

(function (exports, require, module,  filename,  dirname) {    var math = require(‘math‘);    exports.area = function (radius) {        return Math.PI * radius * radius;    };});

  這樣每個模組檔案之間都進行了範圍隔離。封裝之後的代碼會通過vm原生模組的runInThisContext()方法執行(類似eval,只是具有明確上下文,不汙染全域),返回一個具體的function對象。最後,將當前模組對象的exports屬性、require()方法、module(模組對象自身),以及在檔案定位中得到的完整檔案路徑和檔案目錄作為參數傳遞給這個function()執行

  這就是這些變數並沒有定義在每個模組檔案中卻存在的原因。在執行之後,模組的exports屬性被返回給了調用方。exports屬性上的任何方法和屬性都可以被外部調用到,但是模組中的其餘變數或屬性則不可直接被調用

  至此,require、exports、module的流程已經完整,這就是Node對CommonJS模組規範的實現

【C/C++模組的編譯】

  Node調用process.dlopen()方法進行載入和執行。在Node的架構下,dlopen()方法在Windows和*nix平台下分別有不同的實現,通過libuv相容層進行了封裝

  實際上,.node的模組檔案並不需要編譯,因為它是編寫C/C++模組之後編譯產生的,所以這裡只有載入和執行的過程。在執行的過程中,模組的exports對象與.node模組產生聯絡,然後返回給調用者

  C/C++模組給Node使用者帶來的優勢主要是執行效率方面的,劣勢則是C/C++模組的編寫門檻比JavaScript高

【JSON檔案的編譯】

  .json檔案的編譯是3種編譯方式中最簡單的。Node利用fs模組同步讀取JSON檔案的內容之後,調用JSON.parse()方法得到對象,然後將它賦給模組對象的exports,以供外部調用

  JSON檔案在用作項目的設定檔時比較有用。如果你定義了一個JSON檔案作為配置,那就不必調用fs模組去非同步讀取和解析,直接調用require()引入即可。此外,你還可以享受到模組緩衝的便利,並且二次引入時也沒有效能影響

 

CommonJS

  在介紹完Node的模組實現之後,回到頭來再學習下CommonJS規範,相對容易理解

  CommonJS規範的提出,主要是為了彌補當前javascript沒有標準的缺陷,使其具備開發大型應用的基礎能力,而不是停留在小指令碼程式的階段

  CommonJS對模組的定義十分簡單,主要分為模組引用、模組定義和模組標識3個部分

【模組引用】

var math = require(‘math‘);

  在CommonJS規範中,存在require()方法,這個方法接受模組標識,以此引入一個模組的API到當前上下文中

【模組定義】

  在模組中,上下文提供require()方法來引入外部模組。對應引入的功能,上下文提供了exports對象用於匯出當前模組的方法或者變數,並且它是唯一匯出的出口。在模組中,還存在一個module對象,它代表模組自身,而exports是module的屬性。在Node中,一個檔案就是一個模組,將方法掛載在exports對象上作為屬性即可定義匯出的方式:

// math.jsexports.add = function () {    var sum = 0, i = 0,args = arguments, l = args.length;    while (i < l) {        sum += args[i++];    }    return sum;};

  在另一個檔案中,我們通過require()方法引入模組後,就能調用定義的屬性或方法了

// program.jsvar math = require(‘math‘);exports.increment = function (val) {    return math.add(val, 1);};

【模組標識】

  模組標識其實就是傳遞給require()方法的參數,它必須是符合小駝峰命名的字串,或者以.、..開頭的相對路徑,或者絕對路徑。它可以沒有檔案名稱尾碼.js

  模組的定義十分簡單,介面也十分簡潔。它的意義在於將類聚的方法和變數等限定在私人的範圍中,同時支援引入和匯出功能以順暢地串連上下遊依賴。每個模組具有獨立的空間,它們互不干擾,在引用時也顯得乾淨利落

來源地址:http://www.cnblogs.com/xiaohuochai/archive/2017/05/13/6847939.html

【轉】Commonjs規範及Node模組實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.