Commonjs規範及Node模塊實現

来源:http://www.cnblogs.com/xiaohuochai/archive/2017/05/13/6847939.html
-Advertisement-
Play Games

[1]引入 [2]概述 [3]模塊載入 [4]訪問變數 [5]模塊編譯 [6]CommonJS ...


前面的話

  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.js
console.log('aaa');

//b.js
require('./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.js
var a = 100;
global.a = a;

//b.js
require('./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.js
var a = 100;
module.exports.a = a;

//b.js
var 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 .json
Module._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.js
exports.add = function () {
    var sum = 0, i = 0,args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};

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

// program.js
var math = require('math');
exports.increment = function (val) {
    return math.add(val, 1);
};

【模塊標識】

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

  模塊的定義十分簡單,介面也十分簡潔。它的意義在於將類聚的方法和變數等限定在私有的作用域中,同時支持引入和導出功能以順暢地連接上下游依賴。每個模塊具有獨立的空間,它們互不幹擾,在引用時也顯得乾凈利落

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 用bootstrap做了用戶電話號碼查詢的前端頁面。 並且用了MVCPager分頁。 Bootstrap前端頁如下: 一開始使用了用A標簽,分頁成功後,我進入第二頁,點擊這個A標簽,頁面會自動跳轉到第一頁。相當無解。。想不通。經過和群里高手交流說可能是A標簽的問題。。我換成button,問題解決。 ...
  • 1.對話框,輸出框,警告框 1. document.write() 可用於直接向 HTML 輸出流寫內容。簡單的說就是直接在網頁中輸出內容。 2.alert(字元串或變數); 3.confirm 消息對話框通常用於允許用戶做選擇的動作,如:“你對嗎?”等。彈出對話框(包括一個確定按鈕和一個取消按鈕) ...
  • 1.<q>標簽,短文本引用 <q>引用文本</q> <q>標簽的真正關鍵點不是它的預設樣式雙引號(如果這樣我們不如自己在鍵盤上輸入雙引號就行了),而是它的語義:引用別人的話。 2.使用<span>標簽為文字設置單獨樣式 <span>標簽是沒有語義的,它的作用就是為了設置單獨的樣式用的。 語法:<sp ...
  • 代碼: new Ext.form.ComboBox({ store: new Ext.data.JsonStore({ idProperty: 'VehicleNo', url: '../ajax/test.ashx, fields: ['VehicleNo', 'phoneNum'] }), id ...
  • 六、文字排版--刪除線 1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> 5 <title>刪除樣式</title> 6 <style ...
  • ...
  • 1.首先要明確: 誰最終調用函數,this指向誰 this指向的永遠只可能是對象!!!!! this指向誰永遠不取決於this寫在哪,而取決於函數在哪裡調用! this指向的對象,我們稱之為函數的上下文context,也叫做函數的調用者是誰! 2.this指向的規律(與函數調用的方式息息相關) th ...
  • 使用display:inline-block來佈局,別太依賴float ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...