模組機制

MoTong發表於2019-01-29

在聊模組之前,我們先聊聊JS模組化的不足

在實應用中,JavaScript的表現能力取決於宿主環境中的API支援程程。在Web 1.0時,只有對DOM、BOM等基本的支援。隨著Web 2.0的推進 ,HTML5嶄露頭角,它將Web網頁帶進Web應用的時代,在瀏覽器中出現了更多、更強大的API供JavaScript呼叫。但是這些過程發生在前端,後端JavaScript 的規範卻遠遠落後。對於JavaScript自身而言,它的規範是薄弱的,還有以下缺陷。

  • JS沒有模組系統,不支援封閉的作用域和依賴管理
  • 沒有標準庫,沒有檔案系統和IO流API
  • 沒有標準介面
  • 沒有包管理系統

因此,社群也為JavaScript定了相應的規範,其中CommonJS的是最為重要的里程程。

CommonJS規範涵蓋了模組、二進位制、Buffer、字符集編碼、I/O流、單元測試、檔案系統、程式環境、包管理等等。Node能以一中比較成熟的姿態出現,不開CommonJS規範的影響。 下圖是Node與瀏覽器以及w3c組織、CommonJS組織、ECMAScript之間的關係。

關係圖

NodeCommonJS借鑑CommonJSd的的Modules規範實現了一套非常易用的模組系統,NPM對Packages規範的完好支援使得Node應用在開發過程中事半功倍。在本文中,就以Node的模組和包的實現展開說明。

CommonJS的規範

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

在node.js 裡,模組劃分所有的功能,每個JS都是一個模組 實現require方法,NPM實現了模組的自動載入和安裝依賴

(function(exports,require,module,__filename,__dirname){
  exports = module.exports={}
  exports.name = 'zfpx';
  exports = {name:'zfpx'};
  return module.exports;
})
//往下會實現一個簡單的require方法
複製程式碼

模組分類

  1. 原生模組 http path fs util events 編譯成二進位制,載入速度最快,原來模組通過名稱來載入
  2. 檔案模組 在硬碟的某個位置,載入速度非常慢,檔案模組通過名稱或路徑來載入 檔案模組的字尾有三種
  • 字尾名為.js的JavaScript指令碼檔案,需要先讀入記憶體再執行
  • 字尾名為.json的JSON檔案,fs 讀入記憶體 轉化成JSON物件
  • 字尾名為.node的經過編譯後的二進位制C/C++擴充套件模組檔案,可以直接使用

一般自己寫的通過路徑來載入,別人寫的通過名稱去當前目錄或全域性的node_modules下面去找

  1. 第三方模組
  • 如果require函式只指定名稱則視為從node_modules下面載入檔案,這樣的話你可以移動模組而不需要修改引用的模組路徑
  • 第三方模組的查詢路徑包括module.paths和全域性目錄

全域性目錄 window如果在環境變數中設定了NODE_PATH變數,並將變數設定為一個有效的磁碟目錄,require在本地找不到此模組時向在此目錄下找這個模組。 UNIX作業系統中會從 $HOME/.node_modules $HOME/.node_libraries目錄下尋找


模組的載入策略

Node.js模組分為兩類(第三方模組這裡暫時不提),一類是核心模組(即原生模組),一類是檔案模組(我們自己寫的)。原生模組載入速度最快,而檔案模組是動態載入的,載入速度比原生模組慢。但Node對兩類模組都會進行快取,所以在第二次呼叫require的時候,是不會重複呼叫的。 在檔案模組中,又分3類:.js .json .node

require

從檔案模組快取中載入 儘快原生模組與檔案模組的優先順序不同,但是都不會優先於從檔案模組的快取中載入已經存在的模組。

從原生模組載入 原生模組的優先順序僅次於檔案模組快取的優先順序。require方法在解析檔名之後,會先檢查模組是否在原生模組列表中。舉個例子,怡http為例,即使在目錄下存在http.js http.json http.node檔案,但require("http")不會先從這些檔案中載入,而是優先從原生模組中載入。 從檔案載入 當檔案模組快取中不存在,並且不是原生模組的時候,Node.js會解析require傳入的引數,病載入實際的檔案。


整個檔案模組查詢流程

此處輸入圖片的描述
如果require絕對路徑的檔案,就不會去遍歷每一個node_modules目錄,它的速度最快。其餘流程如下:

  1. 從module path陣列中取出第一個目錄作為查詢基準。
  2. 從目錄中查詢該檔案,如果存在,就結束查詢。如果不存在,就進行下一條查詢。
  3. 通過新增.js .json .node字尾查詢,如果存在檔案就結束查詢。如果不存在,則進行下一條。
  4. 將require的引數作為一個包進行查詢,讀取目錄下的package.json檔案,取得main(入口檔案)指定的檔案。
  5. 如果有這個目錄 但是沒有package.json 就會去 當前目錄下查詢index.js index.json ,即重複第3條步驟查詢
  6. 如果仍沒找到,則取出module path陣列中的下一個目錄作為基準查詢,迴圈1-5條步驟
  7. 如果繼續失敗,迴圈1-6個步驟,直到module path中的最後一個值。
  8. 如果仍沒找到就會跑出異常。

整個查詢過程類似原型鏈的查詢和作用域的查詢,但node對路徑查詢實現了快取機制,所以不會很耗效能。


接下來,將會實現一個簡單的require的方法,在此之前,先簡單瞭解一下需要用到的方法:


//引入fs模組
let fs = require('fs');
// fs裡面有一個新增 判斷檔案是否存在
fs.accessSync('./5.module/1.txt'); // 檔案找到了就不會發生任何異常

let path = require('path');// 解決路徑問題

console.log(path.resolve(__dirname,'a')); // 解析絕對路徑
// resolve方法你可以給他一個檔名,他會按照當前執行的路徑 給你拼出一個絕對路徑
// __dirname 當前檔案所在的檔案的路徑 他和cwd有區別

console.log(path.join(__dirname,'a')); // join就是拼路徑用的 可以傳遞多個引數
// 獲取基本路徑 

console.log(path.basename('a.js','.js')); // 經常用來 獲取除了字尾的名字

console.log(path.extname('a.min.js')); // 獲取檔案的字尾名(最後一個.的內容)

console.log(path.posix.delimiter); // window下是分號  maclinux 是:

console.log(path.sep);  // window \  linux /
 
// vm 虛擬機器模組  runinThisContext
let vm = require('vm');//非常像eval,eval可以把字串當成js檔案執行,但它是依賴於環境的

var a = 1;
eval('console.log(a)');//執行這行程式碼的時候,是依賴於var a = 1這個環境的,這個方案會汙染eval的執行結果

var b = 2;
vm.runInThisContext('console.log(b)');//runInThisContext會製造一個乾淨的環境。讓程式碼跑在乾淨的環境裡,乾淨的環境會隔離上面寫的程式碼,輸出:b is not defined
複製程式碼

輸出的結果:

c:\Users\19624\Desktop\201802\5.module\a
c:\Users\19624\Desktop\201802\5.module\a
a
.js
:
\
1
/*
可以在node環境中執行試試看
*/
複製程式碼

require的簡單實現:

let fs = require('fs');
let path = require('path');
let vm = require('vm');
// 自己寫一個模組載入require
// require 出來的是一個模組
function Module(filename){//建構函式,每個模組都應該有個絕對路徑
    this.filename = filename;
    this.exports = {};

}
// 如果沒有字尾的話,希望加js,json還有node的副檔名,副檔名存到建構函式上
Module._extentions = ['.js','.json','.node'];//如果沒有字尾,希望新增上查詢
// 快取的地址
Module._cathe = {};//讀到一個檔案,就往裡放一個, key是它的絕對路徑,值是它的內容
// 解析絕對路徑
Module._resolvePathname = function(filename){
    let p = path.resolve(__dirname,filename);//以當前的資料夾路徑和filename解析
    console.log(path.extname(p))
    if(!path.extname(p)){//判斷檔案是否有字尾名,如果有,則直接返回p,沒有則做進一步處理
        //沒有的話,迴圈Module._extentions,一個個加字尾
        for(var i = 0;i<Module._extentions.length;i++){
            let newPath = p + Module._extentions[i];
            //console.log(newPath);
            // 判斷路徑存不存,如果不存在,會報異常,為了不報錯,使用try catch
            try {//如果訪問的檔案不存在,就會發生異常
                fs.accessSync(newPath);
                // 沒報錯,就返回newPath
                return newPath
            } catch (e) {}
            
        }
    }
    return p;//解析出來的就是一個絕對路徑 
}
// 載入模組
Module.wrapper = [
    "(function(exports,require,module,__dirname,__filename){","\n})"
];
Module.wrap = function(script){
    return Module.wrapper[0] + script + Module.wrapper[1];//拼成一個函式
}
Module._extentions['js'] = function(module){//Module._extentions加了一個屬性,屬性等於函式
    // 如果是js,就同步的讀取出來,然後去exports的值
    // module是個物件,物件裡有 filename exports
    let script = fs.readFileSync(module.filename);
    // 執行的時候,會套個閉包環境
    // (function(exports,require,module,__dirname,filename){})
    let fnStr = Module.wrap(script);//拼成一個函式
    vm.runInThisContext(fnStr).call(module.exports,module,exports,req,module)
};
Module._extentions['json'] =  function(module){
    let script = fs.readFileSync(module.filename);
    // 如果是json直接拿到內容  json.parse即可
    module.exports = JSON.parse(script); 
}
Module.prototype.load = function(filename){//加了prototype是例項呼叫的,不加是建構函式呼叫的
    // 模組可能是json,也可能是js
    let ext = path.extname(filename).slice(1);//.js  .json,slice方法刪除.
    // js用js的方式載入,json用json的方式載入
     Module._extentions[ext](this);//原型中的this,指的是當前Module的例項,看上兩行程式碼Module._extentions['js'],這裡的this傳給module,module代表當前載入的模組

}
function req(filename){//filename是檔名  檔名可能沒有字尾
    // 我們需要弄出一個絕對路徑來,快取是根據絕對路徑來的
    // 先獲取到絕對路徑
    filename = Module._resolvePathname(filename);
    console.log(filename);
    // 先看這個路徑在快取中有沒有,如果有則直接返回
    let catchModule = Module._cathe[filename];
    if(catchModule){
        // 如果是true,表示快取過,就會直接返回快取的exports屬性
        return catchModule.exports
    }
    // 沒快取,載入模組--建立實力
    let module = new Module(filename);//建立模組
    // 將模組的內容載入出來
    module.load(filename);//load是原型鏈上的方法,往上看~
    Module._cache[filename] = module;
    module.loaded = true; // 表示當前模組是否載入完 
    return module.exports;
}

let result = require('./school');
result = req('./school');
console.log(result);
複製程式碼

此時school的檔案是:

// 匯出檔案 才能給其他檔案使用
console.log('載入');
module.exports = 'leo'
複製程式碼

輸出的結果為:

載入
載入
zfpx
複製程式碼

相關文章