欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Nodejs 模塊化實現(xiàn)示例深入探究

 更新時間:2022年11月03日 11:25:30   作者:coder2028  
這篇文章主要為大家介紹了Nodejs 模塊化實現(xiàn)示例深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

正文

本文只討論 CommonJS 規(guī)范,不涉及 ESM

我們知道 JavaScript 這門語言誕生之初主要是為了完成網(wǎng)頁上表單的一些規(guī)則校驗以及動畫制作,所以布蘭登.艾奇(Brendan Eich)只花了一周多就把 JavaScript 設計出來了??梢哉f JavaScript 從出生開始就帶著許多缺陷和缺點,這一點一直被其他語言的編程者所嘲笑。隨著 BS 開發(fā)模式漸漸地火了起來,JavaScript 所要承擔的責任也越來越大,ECMA 接手標準化之后也漸漸的開始完善了起來。

在 ES 6 之前,JavaScript 一直是沒有自己的模塊化機制的,JavaScript 文件之間無法相互引用,只能依賴腳本的加載順序以及全局變量來確定變量的傳遞順序和傳遞方式。而 script 標簽太多會導致文件之間依賴關系混亂,全局變量太多也會導致數(shù)據(jù)流相當紊亂,命名沖突和內存泄漏也會更加頻繁的出現(xiàn)。直到 ES 6 之后,JavaScript 開始有了自己的模塊化機制,不用再依賴 requirejs、seajs 等插件來實現(xiàn)模塊化了。

在 Nodejs 出現(xiàn)之前,服務端 JavaScript 基本上處于一片荒蕪的境況,而當時也沒有出現(xiàn) ES 6 的模塊化規(guī)范(Nodejs 最早從 V8.5 開始支持 ESM 規(guī)范:Node V8.5 更新日志),所以 Nodejs 采用了當時比較先進的一種模塊化規(guī)范來實現(xiàn)服務端 JavaScript 的模塊化機制,它就是 CommonJS,有時也簡稱為 CJS。

這篇文章主要講解 CommonJS 在 Nodejs 中的實現(xiàn)。

一、CommonJS 規(guī)范

在 Nodejs 采用 CommonJS 規(guī)范之前,還存在以下缺點:

  • 沒有模塊系統(tǒng)
  • 標準庫很少
  • 沒有標準接口
  • 缺乏包管理系統(tǒng)

這幾點問題的存在導致 Nodejs 始終難以構建大型的項目,生態(tài)環(huán)境也是十分的貧乏,所以這些問題都是亟待解決的。

CommonJS 的提出,主要是為了彌補當前 JavaScript 沒有模塊化標準的缺陷,以達到像 Java、Python、Ruby 那樣能夠構建大型應用的階段,而不是僅僅作為一門腳本語言。Nodejs 能夠擁有今天這樣繁榮的生態(tài)系統(tǒng),CommonJS 功不可沒。

1.1 CommonJS 的模塊化規(guī)范

CommonJS 對模塊的定義十分簡單,主要分為模塊引用、模塊定義和模塊標識三個部分。下面進行簡單介紹:

1.1.1、模塊引用

示例如下:

const fs = require('fs')

在 CommonJS 規(guī)范中,存在一個 require “全局”方法,它接受一個標識,然后把標識對應的模塊的 API 引入到當前模塊作用域中。

1.1.2、模塊定義

我們已經(jīng)知道了如何引入一個 Nodejs 模塊,但是我們應該如何定義一個 Nodejs 模塊呢?在 Nodejs 上下文環(huán)境中提供了一個 module 對象和一個 exports 對象,module 代表當前模塊,exports 是當前模塊的一個屬性,代表要導出的一些 API。在 Nodejs 中,一個文件就是一個模塊,把方法或者變量作為屬性掛載在 exports 對象上即可將其作為模塊的一部分進行導出。

// add.js
exports.add = function(a, b) {
    return a + b
}

在另一個文件中,我們就可以通過 require 引入之前定義的這個模塊:

const { add } = require('./add.js')
add(1, 2) // print 3

1.1.3、模塊標識

模塊標識就是傳遞給 require 函數(shù)的參數(shù),在 Nodejs 中就是模塊的 id。它必須是符合小駝峰命名的字符串,或者是以.、..開頭的相對路徑,或者絕對路徑,可以不帶后綴名

模塊的定義十分簡單,接口也很簡潔。它的意義在于將類聚的方法和變量等限定在私有的作用于域中,同時支持引入和導出功能以順暢的連接上下游依賴。

CommonJS 這套模塊導出和引入的機制使得用戶完全不必考慮變量污染。

以上只是對于 CommonJS 規(guī)范的簡單介紹,更多具體的內容可以參考:CommonJS規(guī)范

二、Nodejs 的模塊化實現(xiàn)

Nodejs 在實現(xiàn)中并沒有完全按照規(guī)范實現(xiàn),而是對模塊規(guī)范進行了一定的取舍,同時也增加了一些自身需要的特性。接下來我們會探究一下 Nodejs 是如何實現(xiàn) CommonJS 規(guī)范的。

在 Nodejs 中引入模塊會經(jīng)過以下三個步驟:

  • 路徑分析
  • 文件定位
  • 編譯執(zhí)行

在了解具體的內容之前我們先了解兩個概念:

  • 核心模塊:Nodejs 提供的內置模塊,比如 fs、url、http
  • 文件模塊:用戶自己編寫的模塊,比如 Koa、Express

核心模塊在 Nodejs 源代碼的編譯過程中已經(jīng)編譯進了二進制文件,Nodejs 啟動時會被直接加載到內存中,所以在我們引入這些模塊的時候就省去了文件定位、編譯執(zhí)行這兩個步驟,加載速度比文件模塊要快很多。

文件模塊是在運行的時候動態(tài)加載,需要走一套完整的流程:路徑分析、文件定位編譯執(zhí)行等,所以文件模塊的加載速度比核心模塊要慢。

2.1 優(yōu)先從緩存加載

在講解具體的加載步驟之前,我們應當知曉的一點是,Nodejs 對于已經(jīng)加載過一邊的模塊會進行緩存,模塊的內容會被緩存到內存當中,如果下次加載了同一個模塊的話,就會從內存中直接取出來,這樣就省去了第二次路徑分析、文件定位、加載執(zhí)行的過程,大大提高了加載速度。無論是核心模塊還是文件模塊,require() 對同一文件的第二次加載都一律會采用緩存優(yōu)先的方式,這是第一優(yōu)先級的。但是核心模塊的緩存檢查優(yōu)先于文件模塊的緩存檢查。

我們在 Nodejs 文件中所使用的 require 函數(shù),實際上就是在 Nodejs 項目中的 lib/internal/modules/cjs/loader.js 所定義的 Module.prototype.require 函數(shù),只不過在后面的 makeRequireFunction 函數(shù)中還會進行一層封裝,Module.prototype.require 源碼如下:

// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
    validateString(id, 'id');
    if (id === '') {
        throw new ERR_INVALID_ARG_VALUE('id', id,
                                        'must be a non-empty string');
    }
    requireDepth++;
    try {
        return Module._load(id, this, /* isMain */ false);
    } finally {
        requireDepth--;
    }
};

可以看到它最終使用了 Module._load 方法來加載我們的標識符所指定的模塊,找到 Module._load

Module._cache = Object.create(null);
// 這里先定義了一個緩存的對象
// ... ...
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
//    `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
    let relResolveCacheIdentifier;
    if (parent) {
        debug('Module._load REQUEST %s parent: %s', request, parent.id);
        // Fast path for (lazy loaded) modules in the same directory. The indirect
        // caching is required to allow cache invalidation without changing the old
        // cache key names.
        relResolveCacheIdentifier = `${parent.path}\x00${request}`;
        const filename = relativeResolveCache[relResolveCacheIdentifier];
        if (filename !== undefined) {
            const cachedModule = Module._cache[filename];
            if (cachedModule !== undefined) {
                updateChildren(parent, cachedModule, true);
                return cachedModule.exports;
            }
            delete relativeResolveCache[relResolveCacheIdentifier];
        }
    }
    const filename = Module._resolveFilename(request, parent, isMain);
    const cachedModule = Module._cache[filename];
    if (cachedModule !== undefined) {
        updateChildren(parent, cachedModule, true);
        return cachedModule.exports;
    }
    const mod = loadNativeModule(filename, request, experimentalModules);
    if (mod && mod.canBeRequiredByUsers) return mod.exports;
    // Don't call updateChildren(), Module constructor already does.
    const module = new Module(filename, parent);
    if (isMain) {
        process.mainModule = module;
        module.id = '.';
    }
    Module._cache[filename] = module;
    if (parent !== undefined) {
        relativeResolveCache[relResolveCacheIdentifier] = filename;
    }
    let threw = true;
    try {
        module.load(filename);
        threw = false;
    } finally {
        if (threw) {
            delete Module._cache[filename];
            if (parent !== undefined) {
                delete relativeResolveCache[relResolveCacheIdentifier];
            }
        }
    }
    return module.exports;
};

我們可以先簡單的看一下源代碼,其實代碼注釋已經(jīng)寫得很清楚了。

Nodejs 先會根據(jù)模塊信息解析出文件路徑和文件名,然后以文件名作為 Module._cache 對象的鍵查詢該文件是否已經(jīng)被緩存,如果已經(jīng)被緩存的話,直接返回緩存對象的 exports 屬性。否則就會使用 Module._resolveFilename 重新解析文件名,再查詢一邊緩存對象。否則就會當做核心模塊來加載,核心模塊使用 loadNativeModule 方法進行加載。

如果經(jīng)過了以上幾個步驟之后,在緩存中仍然找不到 require 加載的模塊對象,那么就使用 Module 構造方法重新構造一個新的模塊對象。加載完畢之后還會緩存到 Module._cache 對象中,以便下一次加載的時候可以直接從緩存中取到。

從源碼來看,跟我們之前說的沒什么區(qū)別。

2.2 路徑分析

我們知道標識符是進行路徑分析和文件定位的依據(jù),在引用某個模塊的時候我們就會給 require 函數(shù)傳入一個標識符,根據(jù)我們使用的經(jīng)歷不難發(fā)現(xiàn)標識符基本上可以分為以下幾種:

  • 核心模塊:比如 http、fs
  • 文件模塊:這類模塊的標識符是一個路徑字符串,指向工程內的某個文件
  • 非路徑形式的文件模塊:也叫做自定義模塊,比如 connectkoa

標識符類型不同,加載的方式也有差異,接下來我將介紹不同標識符的加載方式。

2.2.1 核心模塊

核心模塊的加載優(yōu)先級僅次于緩存,前文提到過由于核心模塊的代碼已經(jīng)編譯成了二進制代碼,在 Nodejs 啟動的時候就會加載到內存中,所以核心模塊的加載速度非常快。它根本不需要進行路徑分析和文件定位,如果你想寫一個和核心模塊同名的模塊的話,它是不會被加載的,因為其加載優(yōu)先級不如核心模塊。

2.2.2 路徑形式的文件模塊

當標識符為路徑字符串時,require 都會把它當做文件模塊來加載,在根據(jù)標識符獲得真實路徑之后,Nodejs 會將真實路徑作為鍵把模塊緩存到一個對象里,使二次加載更快。

由于文件模塊的標識符指明了模塊文件的具體位置,所以加載速度相對而言也比較快。

2.2.3 自定義模塊

自定義模塊是一個包含 package.json 的項目所構造的模塊,它是一種特殊的模塊,其查找方式比較復雜,所以耗時也是最長的。

在 Nodejs 中有一個叫做模塊路徑的概念,我們新建一個 module_path.js 的文件,然后在其中輸入如下內容:

console.log(module.paths)

然后使用 Nodejs 運行:

node module_path.js

我們可以看到控制臺輸入大致如下:

[ 'C:\\Users\\UserName\\Desktop\\node_modules',  'C:\\Users\\UserName\\node_modules',  'C:\\Users\\node_modules',  'C:\\node_modules' ]

此時我的 module_path.js 文件是放在桌面的,所以可以看到這個文件模塊的模塊路徑是當前文件同級目錄下的 node_modules,如果找不到的話就從父級文件夾的同名目錄下找,知道找到根目錄下。這種查找方式和 JavaScript 中的作用域鏈非常相似??梢钥吹疆斘募窂皆缴畹臅r候查找所耗時間越長,所以這也是自定義模塊加載速度最慢的原因。

在 Windows 環(huán)境中,Nodejs 通過下面函數(shù)獲取模塊路徑:

Module._nodeModulePaths = function(from) {
    // Guarantee that 'from' is absolute.
    from = path.resolve(from);
    // note: this approach *only* works when the path is guaranteed
    // to be absolute.  Doing a fully-edge-case-correct path.split
    // that works on both Windows and Posix is non-trivial.
    // return root node_modules when path is 'D:\\'.
    // path.resolve will make sure from.length >=3 in Windows.
    if (from.charCodeAt(from.length - 1) === CHAR_BACKWARD_SLASH &&
        from.charCodeAt(from.length - 2) === CHAR_COLON)
        return [from + 'node_modules'];
    const paths = [];
    var p = 0;
    var last = from.length;
    for (var i = from.length - 1; i >= 0; --i) {
        const code = from.charCodeAt(i);
        // The path segment separator check ('\' and '/') was used to get
        // node_modules path for every path segment.
        // Use colon as an extra condition since we can get node_modules
        // path for drive root like 'C:\node_modules' and don't need to
        // parse drive name.
        if (code === CHAR_BACKWARD_SLASH ||
            code === CHAR_FORWARD_SLASH ||
            code === CHAR_COLON) {
            if (p !== nmLen)
                paths.push(from.slice(0, last) + '\\node_modules');
            last = i;
            p = 0;
        } else if (p !== -1) {
            if (nmChars[p] === code) {
                ++p;
            } else {
                p = -1;
            }
        }
    }
    return paths;
};

代碼和注釋都寫得很明白,大家看看就行,常量都放在 /lib/internal/constants.js 這個模塊。

2.3 文件定位

2.3.1 文件擴展名分析

我們在引用模塊的很多時候,傳遞的標識符都不會攜帶擴展名,比如

// require('./internal/constants.js')
require('./internal/constants')

很明顯下面的方式更簡潔,但是 Nodejs 在定位文件的時候還是會幫我們補齊。補齊的順序依次為:.js、.json.node,在補齊的時候 Nodejs 會依次進行嘗試。在嘗試的時候 Nodejs 會調用 fs 模塊來判斷文件是否存在,所以這里可能會存在性能問題,如果在引用模塊的時候加上擴展名,可以使得模塊加載的速度變得更快。

在 Nodejs 源碼 中,我們可以看到當解析不到文件名的時候,會嘗試使用 tryExtensions 方法來添加擴展名:

if (!filename) {
    // Try it with each of the extensions
    if (exts === undefined)
        exts = Object.keys(Module._extensions);
    filename = tryExtensions(basePath, exts, isMain);
}

而嘗試的擴展名就是 Module._extensions 的鍵值,檢索代碼不難發(fā)現(xiàn)代碼中依次定義了 .js、.json.node、.mjs 等鍵,所以 tryExtensions 函數(shù)會依次進行嘗試:

// Given a path, check if the file exists with any of the set extensions
function tryExtensions(p, exts, isMain) {
    for (var i = 0; i < exts.length; i++) {
        const filename = tryFile(p + exts[i], isMain);
        if (filename) {
            return filename;
        }
    }
    return false;
}

其中又調用了 tryFile 方法:

function tryFile(requestPath, isMain) {
    const rc = stat(requestPath);
    if (preserveSymlinks && !isMain) {
        return rc === 0 && path.resolve(requestPath);
    }
    return rc === 0 && toRealPath(requestPath);
}
// Check if the file exists and is not a directory
// if using --preserve-symlinks and isMain is false,
// keep symlinks intact, otherwise resolve to the
// absolute realpath.
function tryFile(requestPath, isMain) {
    const rc = stat(requestPath);
    if (preserveSymlinks && !isMain) {
        return rc === 0 && path.resolve(requestPath);
    }
    return rc === 0 && toRealPath(requestPath);
}
// 這個函數(shù)在其他地方還有用到,比較重要
function toRealPath(requestPath) {
    return fs.realpathSync(requestPath, {
        [internalFS.realpathCacheKey]: realpathCache
    });
}

可以看到最終還是依賴了 fs.realpathSync 方法,所以這里就跟之前說的是一樣的,可能會存在性能問題,如果我們直接帶上了擴展名的話,直接就可以解析出 filename,就不會去嘗試擴展名了,這樣可以稍微提高一點加載速度。

2.3.2 目錄和包分析

我們寫的文件模塊可能是一個 npm 包,此時包內包含許多 js 文件,所以 Nodejs 加載的時候又需要定位文件。Nodejs 會查找 package.json 文件,使用 JSON.stringify 來解析 json,隨后取出其 main 字段之后對文件進行定位,如果文件名缺少擴展的話,也會進入擴展名嘗試環(huán)節(jié)。

如果 main 字段指定的文件名有誤,或者壓根沒有 package.json 文件,那么 Nodejs 會將 index 當做默認文件名,隨后開始嘗試擴展名。

2.4 模塊編譯

Nodejs 中每一個模塊就是一個 Module類實例,Module 的構造函數(shù)如下:

function Module(id = '', parent) {
    this.id = id;
    this.path = path.dirname(id);
    this.exports = {};
    this.parent = parent;
    updateChildren(parent, this, false);
    this.filename = null;
    this.loaded = false;
    this.children = [];
}

編譯和執(zhí)行是引入文件模塊的最后一個環(huán)節(jié),定位到具體文件后,Nodejs 會新建一個模塊對象,然后根據(jù)路徑載入緩存以后進行編譯,擴展名不同,編譯的方式也不同,它們的編譯方法都注冊在了 Module._extensions 對象上,前文有提到過:

  • .js 文件:通過同步讀取文件內容后編譯執(zhí)行
  • .json 文件:通過 fs 模塊讀取文件,之后使用 JSON.parse 轉化成 JS 對象
  • .node 文件:這是使用 C/C++ 編寫的擴展模塊,通過內置的 dlopen 方法加載最后編譯生成的文件
  • .mjs 文件:這是 Nodejs 支持 ESM 加載方式的模塊文件,所以使用 require 方法載入的時候會直接拋出錯誤

在 Nodejs 的 輔助函數(shù)模塊 中,通過以下代碼把 Module._extensions 傳遞給了 require 函數(shù):

// Enable support to add extra extension types.require.extensions = Module._extensions;

所以我們可以通過在模塊中打印 require.extensions 查看當前 Nodejs 能夠解析的模塊:

console.log(require.extensions)
// { '.js': [Function], '.json': [Function], '.node': [Function] }

另外我們可以看到上面第二段代碼中的注釋:Enable support to add extra extension types,也就是說我們可以通過修改 require.extensions 對象來注冊模塊的解析方法。

比如我們有一個 .csv 文件,我們想把它解析成一個二維數(shù)組,那么我們就可以寫一下方法注冊:

const fs = require('fs')
// 注冊解析方法到 require.extensions 對象
require.extensions['.csv'] = function(module, filename) {
    // module 是當前模塊的 Module 實例,filename 是當前文件模塊的路徑
    const content = fs.readFileSync(filename, 'utf8'),
          lines = content.split(/\r\n/)
    const res = lines.map(line =&gt; line.split(','))
    // 注意導出是通過給 module.exports 賦值,而不是用 return
    module.exports = res
}
/*
*    demo.csv 的內容為:
*    1,2,3
*    2,3,4
*    5,6,7
*/
const arr = require('./demo.csv')
console.log(arr)
// output
// [ [ '1', '2', '3' ], [ '2', '3', '4' ], [ '5', '6', '7' ] ]

但是在 v0.10.6 開始 Nodejs 就不再推薦使用這種方式來擴展加載方式了,而是期望現(xiàn)將其他語言轉化為 JavaScript 以后再加載執(zhí)行,這樣就避免了將復雜的編譯加載過程引入到 Nodejs 的執(zhí)行過程。

接下來我們了解一下 Nodejs 內置的幾種模塊的加載方式。

2.4.1 JavaScript 模塊的編譯

在我們編寫 Nodejs 模塊的時候我們可以隨意的使用 require、modulemodule、__dirname__filename 等變量,仿佛它們都是 Nodejs 內置的全局變量一樣,但是實際上他們都是局部變量。在 Nodejs 加載 JavaScript 模塊的時候,會自動將模塊內的所有代碼包裹到一個匿名函數(shù)內,構成一個局部作用域,順便把 require……等變量傳入了匿名函數(shù)內部,所以我們的代碼可以隨意使用這些變量。

假設我們的模塊代碼如下:

exports.add = (a, b) => a + b

經(jīng)過 Nodejs 加載之后,代碼變成了下面這樣:

(function(exports, require, module, __filename, __dirname) {
    exports.add = (a, b) => a + b
})

這樣看起來的話,一切都變得很順其自然了。這也是為什么每個模塊都是獨立的命名空間,在模塊文件內隨便命名變量而不用擔心全局變量污染,因為這些變量都定義在了函數(shù)內部,成為了這個包裹函數(shù)的私有變量。

弄明白 Nodejs 加載 JavaScript 的原理之后,我們很容易就可以弄明白為什么不能給 exports 直接賦值了,根本原因就在于 JavaScript 是一門按值傳遞(Pass-by-Value)的語言,不管我們給變量賦值的是引用類型還是原始類型,我們得到變量得到的都是一個值,只不過賦值引用類型時,變量得到的是一個代表存儲引用類型的內存地址值(可以理解為指針),而我們使用變量時 JavaScript 會根據(jù)這個值去內存中找到對應的引用類型值,所以看起來也像是引用傳遞。而一旦我們給 exports 這種變量重新賦值的時候,exports 就失去了對原來引用類型的指向,轉而指向新的值,所以就會導致我們賦給 exports 的值并沒有指向原來的引用類型對象。

看看下面這段代碼:

function changeRef(obj) {
    obj = 12
}
const ref = {}
changeRef(ref)
console.log(ref) // {}

可以看到函數(shù)內對 obj 重新賦值根本不影響函數(shù)外部的 ref對象,所以如果我們在模塊內(及包裹函數(shù)內)修改 exports 的指向的話,外部的 module.exports 對象根本不受影響,我們導出的操作也就失敗了。

下面我們稍微看一下 Nodejs 源碼是如何編譯執(zhí)行 JavaScript 代碼的。

首先根據(jù) Module._extensions 對象上注冊的 .js 模塊加載方法找到入口:

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
    const content = fs.readFileSync(filename, 'utf8');
    module._compile(content, filename);
};

可以看到加載方法聽過 fs.readFileSync 方法同步讀取了 .js 的文件內容之后,就把內容交給 module_compile 方法去處理了,這個方法位于 Module 類的原型上,我們繼續(xù)找到 Module.prototype._compile 方法:

// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.Module.prototype._compile = function(content, filename) {
    let moduleURL;
    let redirects;
    if (manifest) {
        moduleURL = pathToFileURL(filename);
        redirects = manifest.getRedirects(moduleURL);
        manifest.assertIntegrity(moduleURL, content);
    }
    const compiledWrapper = wrapSafe(filename, content);
    var inspectorWrapper = null;
    if (getOptionValue('--inspect-brk') &amp;&amp; process._eval == null) {
        if (!resolvedArgv) {
            // We enter the repl if we're not given a filename argument.
            if (process.argv[1]) {
                resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
            } else {
                resolvedArgv = 'repl';
            }
        }
        // Set breakpoint on module start
        if (!hasPausedEntry &amp;&amp; filename === resolvedArgv) {
            hasPausedEntry = true;
            inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
        }
    }
    const dirname = path.dirname(filename);
    const require = makeRequireFunction(this, redirects);
    var result;
    const exports = this.exports;
    const thisValue = exports;
    const module = this;
    if (requireDepth === 0) statCache = new Map();
    if (inspectorWrapper) {
        result = inspectorWrapper(compiledWrapper, thisValue, exports,
                                  require, module, filename, dirname);
    } else {
        result = compiledWrapper.call(thisValue, exports, require, module,
                                      filename, dirname);
    }
    if (requireDepth === 0) statCache = null;
    return result;
};

可以看到最后還是交給了 compiledWrapper 方法來處理模塊內容(inspectWrapper 是做斷電調試用的,咱們可以不管它),繼續(xù)看 compiledWrapper 方法。

compiledWrapper 方法來源于 wrapSafe 的執(zhí)行結果:

const compiledWrapper = wrapSafe(filename, content);

wrapSafe 函數(shù)的定義如下:

function wrapSafe(filename, content) {
    if (patched) {
        const wrapper = Module.wrap(content);
        return vm.runInThisContext(wrapper, {
            filename,
            lineOffset: 0,
            displayErrors: true,
            importModuleDynamically: experimentalModules ? async (specifier) => {
                const loader = await asyncESM.loaderPromise;
                return loader.import(specifier, normalizeReferrerURL(filename));
            } : undefined,
        });
    }
    const compiled = compileFunction(
        content,
        filename,
        0,
        0,
        undefined,
        false,
        undefined,
        [],
        [
            'exports',
            'require',
            'module',
            '__filename',
            '__dirname',
        ]
    );
    if (experimentalModules) {
        const { callbackMap } = internalBinding('module_wrap');
        callbackMap.set(compiled.cacheKey, {
            importModuleDynamically: async (specifier) => {
                const loader = await asyncESM.loaderPromise;
                return loader.import(specifier, normalizeReferrerURL(filename));
            }
        });
    }
    return compiled.function;
}
// Module.wrap
// eslint-disable-next-line func-style
let wrap = function(script) {
    return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];
Object.defineProperty(Module, 'wrap', {
    get() {
        return wrap;
    },
    set(value) {
        patched = true;
        wrap = value;
    }
});

上面這段代碼可以看到 wrapSafe 方法通過 Module.wrap 將模塊代碼構造成了一個匿名函數(shù),隨后扔給了 vm.runInThisContext 或者 compileFunction 去執(zhí)行,這兩函數(shù)都開始涉及到 JavaScript 跟 C/C++ 的底層了,作者水平渣渣,不再進行下一步解讀,感興趣的童鞋可以自己找到源碼繼續(xù)閱讀。

2.4.2 C/C++ 模塊的編譯

Nodejs 通過調用 process.dlopen 加載和執(zhí)行 C/C++ 模塊,該函數(shù)在 Window 和 *nix 系統(tǒng)下有不同的實現(xiàn),通過 linuv 兼容層進行了封裝。

實際上 .node 模塊不需要編譯,因為是根據(jù) C/C++ 編譯而成的,所以只有加載和執(zhí)行過程。編寫 C/C++ 模塊能夠提高 Nodejs 的擴展能力和計算能力,我們知道 Nodejs 是單線程異步無阻塞的語言,優(yōu)勢在于 IO 密集型場景而非計算密集型場景。當我們有大量的計算操作需要執(zhí)行時,我們可以將計算操作放到 C/C++ 模塊中執(zhí)行,這樣可以提升 Nodejs 在計算密集型場景下的表現(xiàn)。但是 C/C++ 的編程門檻比 Nodejs 高很多,所以這也是一大缺點。

Nodejs 在 v10.x 中引入了 Worker Threads 特性,并且這一特性在 v12.x 中開始默認啟用,大大提高了 Nodejs 在計算密集型場景下的表現(xiàn),在某種程度上減少了開發(fā)者所需要編寫的 C/C++ 代碼量。

2.4.3 JSON 文件的編譯

JSON 文件的編譯是最簡單的,通過 fs.readFileSync 讀取文件內容后,調用 JSON.parse 轉化成 JavaScript 對象導出就行了。

由于作者水平有限,關于核心模塊以及 C/C++ 模塊的書寫和編譯不再講解。

三、總結

通過這篇文章,我們至少學習到了以下幾點:

CommonJS 模塊化規(guī)范的基本內容

CommonJS 規(guī)范主要包括 模塊引用模塊定義模塊標識,規(guī)定了一個模塊從引入到消費以及導出的整個過程。通過給 require 方法傳遞模塊標識符(路徑字符串或者模塊名稱)來引入 CJS 模塊,導出時給 module.exports 或者 exports 賦值或者添加屬性即可。

Nodejs 引入模塊的加載順序和基本步驟

1、加載順序和速度:

require 函數(shù)接收到模塊標識符時,會優(yōu)先檢查內存中是否已經(jīng)有緩存的模塊對象,有的話直接返回,沒有就繼續(xù)查找。所以緩存的加載優(yōu)先級和加載速度是最高的,其次是核心模塊,因為核心模塊已經(jīng)被編譯到了 Nodejs 代碼中,Nodejs 啟動的時候就已經(jīng)把核心模塊的內容加載到了內存中,所以核心模塊的加載順序和加載速度位于第二,僅次于內存。然后就是文件模塊,Nodejs 通過找到文件然后使用對應的方法加載文件中的代碼并執(zhí)行。最后才是自定義模塊。

2、加載基本步驟:

加載步驟大概有路徑分析、文件定位編譯執(zhí)行三個過程。

Nodejs 在拿到模塊標識符之后,會進行路徑分析,獲得了入口文件的絕對路徑之后就會去內存檢索,如果內存中沒有緩存的話就會進入下一步,進行文件定位。注意自定義模塊會有個 模塊路徑 的概念,加載自定義模塊時會首先在當前文件的同級 node_modules 目錄下查找,如果沒有找到的話就向上一級繼續(xù)查找 node_modules,直到系統(tǒng)根目錄(Windows 的盤符目錄,比如 C:\ 或者 *nix 的根目錄 /),所以自定義模塊的加載耗時最長。

路徑分析之后會進行文件定位,嘗試多種不同的擴展名然后判斷文件是否存在,如果最終都不存在的話就會繼續(xù)把這個模塊當做自定義模塊進行加載,如果還是找不到就直接報錯。擴展判斷的順序依次為 .js、.json.node。

Nodejs 對于不同模塊的編譯方式

  • JavaScript 模塊通過包裹函數(shù)包裹之后交給系統(tǒng)函數(shù)運行
  • JSON 模塊通過 JSON.parse 轉化為 JavaScript 對象然后返回結果
  • C/C++ 模塊通過系統(tǒng)級的 process.dlopen 函數(shù)加載執(zhí)行

以上就是Nodejs 模塊化實現(xiàn)示例深入探究的詳細內容,更多關于Nodejs 模塊化的資料請關注腳本之家其它相關文章!

相關文章

  • 學習Node.js模塊機制

    學習Node.js模塊機制

    這篇文章主要為大家詳細介紹了Node.js模塊機制,一篇關于Node.js模塊機制的學習筆記,感興趣的小伙伴們可以參考一下
    2016-10-10
  • node.js文件上傳處理示例

    node.js文件上傳處理示例

    這篇文章主要介紹了node.js文件上傳處理的相關資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下。
    2016-10-10
  • node+vue實現(xiàn)用戶注冊和頭像上傳的實例代碼

    node+vue實現(xiàn)用戶注冊和頭像上傳的實例代碼

    本篇文章主要介紹了node+vue實現(xiàn)用戶注冊和頭像上傳的實例代碼,具有一定的參考價值,有興趣的可以了解一下
    2017-07-07
  • node.js基礎知識小結

    node.js基礎知識小結

    本文給大家匯總介紹了學習node.js的一些關于開發(fā)環(huán)境的基礎知識,非常簡單,給新手們參考下
    2018-02-02
  • Node.js處理多個請求的技巧和方法

    Node.js處理多個請求的技巧和方法

    Node.js在處理多個請求方面具有優(yōu)勢,它利用事件驅動和非阻塞式I/O的特性,能夠高效地處理并發(fā)請求,提供快速響應和良好的可擴展性,這篇文章主要介紹了Node.js如何處理多個請求,需要的朋友可以參考下
    2023-11-11
  • 詳解NodeJS框架express的路徑映射(路由)功能及控制

    詳解NodeJS框架express的路徑映射(路由)功能及控制

    這篇文章主要介紹了詳解NodeJS框架express的路徑映射(路由)功能及控制,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-03-03
  • node.js報錯:npm?ERR?code?EPERM的解決過程

    node.js報錯:npm?ERR?code?EPERM的解決過程

    在學習vue+typescript的時候突然發(fā)現(xiàn)了個錯誤,所以下面這篇文章主要給大家介紹了關于node.js報錯:npm?ERR?code?EPERM的詳細解決過程,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-08-08
  • 從零開始學習Node.js

    從零開始學習Node.js

    這篇文章主要介紹了從零開始學習Node.js結合具體實例形式分析了使用方法與相關注意事項,需要的朋友可以參考下,希望能夠給你帶來幫助
    2021-09-09
  • express如何解決ajax跨域訪問session失效問題詳解

    express如何解決ajax跨域訪問session失效問題詳解

    這篇文章主要給大家介紹了關于express如何解決ajax跨域訪問session失效問題的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-06-06
  • node.js中對Event Loop事件循環(huán)的理解與應用實例分析

    node.js中對Event Loop事件循環(huán)的理解與應用實例分析

    這篇文章主要介紹了node.js中對Event Loop事件循環(huán)的理解與應用,結合實例形式分析了node.js中Event Loop事件循環(huán)相關原理、使用方法及操作注意事項,需要的朋友可以參考下
    2020-02-02

最新評論