模塊機制

在聊模塊以前,咱們先聊聊JS模塊化的不足javascript

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

  • JS沒有模塊系統,不支持封閉的做用域和依賴管理
  • 沒有標準庫,沒有文件系統和IO流API
  • 沒有標準接口
  • 沒有包管理系統

所以,社區也爲JavaScript定了相應的規範,其中CommonJS的是最爲重要的里程程。java

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

關係圖

NodeCommonJS借鑑CommonJSd的的Modules規範實現了一套很是易用的模塊系統,NPM對Packages規範的無缺支持使得Node應用在開發過程當中事半功倍。在本文中,就以Node的模塊和包的實現展開說明。linux

CommonJS的規範

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

在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
複製代碼
相關文章
相關標籤/搜索