首頁 > webfront > ECMAS > nodejs > > 正文

再談Node.js的模塊加載方式+機制與運行原理

點擊:

模塊的加載require('__')||import '__' 這里應該怎么填?為什么require('express')可以直接加載?他的加載順序是什么?加載原理是什么?

此篇主要由以下幾篇神文凝練而成

Node.js的模塊載入方式與機制

模塊以及加載機制,主要討論找不到模塊的問題

結合源碼分析 Node.js 模塊加載與運行原理

npm 模塊加載機制詳解


相對路徑指定模塊,一般用于加載自己的模塊。

    必須用到的符號:

         ./ 表示當前目錄,相對路徑所相對的就是當前的目錄

         ../ 表示上一級模塊,可以無限使用直到跳轉到根目錄

nodejs的模塊分類

  • 核心模塊:包含在 Node.js 源碼中,被編譯進 Node.js 可執行二進制文件 JavaScript 模塊,也叫 native 模塊,比如常用的 http, fs 等等

  • C/C++ 模塊,也叫 built-in 模塊,一般我們不直接調用,而是在 native module 中調用,然后我們再 require

  • native原生模塊:http  fs path等,這些模塊都在源碼包的lib目錄下面,nodejs安裝好之后是找不到這些模塊的,都作為node.exe的一部分了,require這些模塊永遠沒問題的,如果哪天出現問題了,直接重啟電腦或者重裝node。有什么疑問可以通過下載源碼對這些原生模塊的功能進行查看。地址:https://nodejs.org/download/。

  • 第三方模塊-文件模塊:非 Node.js 源碼自帶的模塊都可以統稱第三方模塊,比如 express,webpack 等等。

    JavaScript 模塊,這是最常見的,我們開發的時候一般都寫的是 JavaScript 模塊

    JSON 模塊,這個很簡單,就是一個 JSON 文件

    C/C++ 擴展模塊,使用 C/C++ 編寫,編譯之后后綴名為 .node

  • 自定義模塊:我們自己寫的模塊,之所以獨立出來是因為其加載和另兩種模塊有區別。

怎樣定義模塊

nodejs聲明一個模塊有2中做法

    exports.module_name

    module.exports

關于這兩個的區別也很簡單,不過要講明白很費勁,關鍵點在于知道有 module 這個全局變量的存在,打印出來并做幾次嘗試,就完全明白了,這里有一篇非常精彩  的關于這兩者異同的文章:《nodejs中exports與module.exports的實踐

第三方模塊安裝在哪(NPM)

幾條命令

npm config get/set prefix //查看設置全局安裝目錄,全局安裝的模塊就安裝該目錄下面的node_modules目錄下
npm install [-g]  // -g 全局安裝,模塊將會安裝到全局目錄下。不帶 -g 則直接安裝在當前所在目錄下,即為本地安裝

 模塊的存在形式

  1、文件包含,這個比較直觀,直接指定到文件名(去掉 .js 后綴),就可以得到文件里面所有導出的模塊。

  2、文件夾包含,通過npm安裝的第三方模塊都是這種方式,指定到模塊所在的文件夾,該文件夾就是模塊名,以express為例:


express文件結構

模塊加載機制:

首先搜索當前目錄下的 package.json 文件,查找里面的mian屬性,如果存在,則加載該屬性所指定的的文件。如果不存在 package.json 或者該文件里面沒有main字段,nodejs將試圖加載 index.js 

都不存在那么就只有說一聲Cannot find module了。


node模塊的載入及緩存機制

  1. 載入內置模塊(A Core Module)

  2. 載入文件模塊(A File Module)

  3. 載入文件目錄模塊(A Folder Module)

  4. 載入node_modules里的模塊

  5. 自動緩存已載入模塊

 

一、載入內置模塊

Node的內置模塊被編譯為二進制形式,引用時直接使用名字而非文件路徑。當第三方的模塊和內置模塊同名時,內置模塊將覆蓋第三方同名模塊。因此命名時需要注意不要和內置模塊同名。如獲取一個http模塊

    var http = require('http')

返回的http即是實現了HTTP功能Node的內置模塊。

二、載入文件模塊

  • 絕對路徑的:var myMod = require('/home/base/my_mod')

  • 相對路徑的:var myMod = require('./my_mod')

注意,這里忽略了擴展名“.js”,以下是對等的

    var myMod = require('./my_mod')

    var myMod = require('./my_mod.js')


三、載入文件目錄模塊

可以直接require一個目錄,假設有一個目錄名為folder,如

    var myMod = require('./folder')

此時,Node將搜索整個folder目錄,Node會假設folder為一個包并試圖找到包定義文件package.json。如果folder目錄里沒有包含package.json文件,Node會假設默認主文件為index.js,即會加載index.js。如果index.js也不存在,那么加載將失敗。

+folder

    index.js

    modA.js

    package.json

+init.js

package.json定義如下

{
    "name": "pack",
    "main": "modA.js"
}

此時 require('./folder') 將返回模塊modA.js。如果package.json不存在,那么將返回模塊index.js。如果index.js也不存在,那么將發生載入異常。

四、載入node_modules里的模塊

如果模塊名不是路徑,也不是內置模塊,Node將試圖去當前目錄的node_modules文件夾里搜索。如果當前目錄的node_modules里沒有找到,Node會從父目錄的node_modules里搜索,這樣遞歸下去直到根目錄。

不必擔心,npm命令可讓我們很方便的去安裝,卸載,更新node_modules目錄。

五、自動緩存已載入模塊

對于已加載的模塊Node會緩存下來,而不必每次都重新搜索。下面是一個示例

modA.js

console.log('模塊modA開始加載...')
exports = function() {
    console.log('Hi')
}
console.log('模塊modA加載完畢')

init.js

var mod1 = require('./modA')
var mod2 = require('./modA')
console.log(mod1 === mod2)

雖然require了兩次,但modA.js仍然只執行了一次。mod1和mod2是相同的,即兩個引用都指向了同一個模塊對象。

模塊在每一次加載之后都會被緩存起來。這也就意味著在每一次使用require(‘foo’)時,返回的都是同一個對象,即使文件隨后被修改過。


多次執行require('./modA')并不會導致模塊代碼被執行多次,這是一個很重要的功能。

而如果你確實是想讓一個模塊的代碼被執行多次,那么就導出一個函數,然后多次調用那個函數。

模塊緩存注意事項

模塊緩存基于它們的resolved filename。由于被調用的模塊的位置[從node_modules文件夾中加載]不同,所以模塊可能會被解析為不同的文件名。

如果解析的結果是不同的文件,但卻總是返回相同的對象,這并不是我們所希望的結果。

此外,在大小寫不敏感的文件系統或操作系統上,不同的文件名可能會指向相同的文件,但緩存系統依舊會將它們視為不同的模塊,并多次加載文件。例如,require(‘./foo’) 和 require(‘./FOO’)不管./foo或./FOO是否為同一個文件,都將會返回兩個不同的對象。

require('__')||import '__' 這里應該怎么填

  • 相對路徑指定模塊,一般用于加載自己的模塊。必須用到的符號:

        ./ 表示當前目錄,相對路徑所相對的就是當前的目錄

        ../ 表示上一級模塊,可以無限使用直到跳轉到根目錄

  • 絕對路徑指定模塊地址,除了原生模塊之外,任何文件模塊都可以加載到,除非路徑出錯了。

    比如我們可以這樣子加載express模塊 '/www/node_module/express/'

  • 直接使用 require('xxx') 那么所加載的模塊要么是原生模塊,要么該模塊在某個node_modules目錄下面

混合模式

為了確定通過require()函數來加載時,調用的明確的文件名,我們可以使用

require.resolve()函數。

能將上述的方式搭配利用,需要的就是require.resolve方法體現出來的高水平算法。

文件的路徑為Y,require(x):

  1. 如果X是核心模塊
    a. 返回核心模塊
    b. 結束代碼

  2. 如果 X 以 ‘./‘ 或 ‘/‘ 或 ‘../‘開始
    a. 以文件的形式加載(Y + X)
    b. 以目錄的形式加載(Y + X)

  3. 以NODE_MODULES的形式來加載(X, dirname(Y))

  4. 拋出異常 “not found”

以文件的形式加載的具體說明

1.如果存在文件x,那么就把x以javascript文本的形式來加載
1.如果存在文件x.js,那么就把x.js以javascript文本的形式來加載
3.如果存在文件x.json,  那么就解析 x.json 為一個 JavaScript 對象
4.如果存在文件x.node, 那么就把 x.node 作為一個二進制文件來加載

以目錄的形式加載的具體說明

  1. 如果存在文件 X/package.json,
    a. 解析 X/package.json, 然后尋在main字段.
    b. let M = X + (json 的main字段)
    c. 以文件的形式加載(M)

  2. 如果存在文件X/index.js, 把 X/index.js 以javascript文本的形式來加載

  3. 如果存在文件X/index.json, 解析 X/index.json 為一個 JavaScript 對象

  4. 如果存在文件X/index.node, 把 X/index.node 作為一個二進制文件來加載

以NODE_MODULES的形式來加載(X, START)的具體說明

  1. let DIRS=NODE_MODULES_PATHS(START)

  2. for each DIR in DIRS:
    a. 以文件的形式加載(DIR/X)
    b. 以目錄的形式加載(DIR/X)

NODE_MODULES_PATHS(START)函數的具體說明

  1. let PARTS = path split(START)

  2. let I = count of PARTS - 1

  3. let DIRS = []

  4. while I >= 0,
    a. if PARTS[I] = “node_modules” CONTINUE
    c. DIR = path join(PARTS[0 .. I] + “node_modules”)
    b.    DIRS = DIRS + DIR
    c. let I = I - 1

  5. return DIRS

nodejs模塊運行分析

假設有一個 index.js 文件,里面只有一行很簡單的 console.log('hello world') 代碼。當輸入 node index.js 的時候,Node.js 是如何編譯、運行這個文件的呢?

從《結合源碼分析 Node.js 模塊加載與運行原理》,

可以看到,主要是調用 Module._load 來加載執行 process.argv[1]。

下文我們在分析模塊的 require 的時候,也會來到 lib/module.js 中,也會分析到 Module._load。因此我們可以看出,Node.js 啟動一個文件的過程,其實到最后,也是 require 一個文件的過程,可以理解為是立即 require 一個文件

分析 require 的原理

var http = require('http');

那么當執行這一句代碼的時候,會發生什么呢?

  • 先生成 cacheKey,判斷相應 cache 是否存在,若存在直接返回

  • 如果 path 的最后一個字符不是 /

    • 判斷路徑如果存在,直接返回

    • 嘗試在路徑后面加上 .js, .json, .node 三種后綴名,判斷是否存在,存在則返回

    • 嘗試在路徑后面依次加上 index.js, index.json, index.node,判斷是否存在,存在則返回

    • 如果路徑是一個文件并且存在,那么直接返回文件的路徑

    • 如果路徑是一個目錄,調用 tryPackage 函數去解析目錄下的 package.json,然后取出其中的 main 字段所寫入的文件路徑

    • 如果還不成功,直接對當前路徑加上 .js, .json, .node 后綴名進行嘗試

  • 如果 path 的最后一個字符是 /

    • 調用 tryPackage ,解析流程和上面的情況類似

    • 如果不成功,嘗試在路徑后面依次加上 index.js, index.json, index.node,判斷是否存在,存在則返回

process1

這里目前還沒有如何跟直白簡練的文字來概括此文內容,所以,及不copy了