在聊模組之前,我們先聊聊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方法
複製程式碼
模組分類
- 原生模組
http
path
fs
util
events
編譯成二進位制,載入速度最快,原來模組通過名稱來載入 - 檔案模組
在硬碟的某個位置,載入速度非常慢,檔案模組通過名稱或路徑來載入 檔案模組的字尾有三種
- 字尾名為.js的JavaScript指令碼檔案,需要先讀入記憶體再執行
- 字尾名為.json的JSON檔案,fs 讀入記憶體 轉化成JSON物件
- 字尾名為.node的經過編譯後的二進位制C/C++擴充套件模組檔案,可以直接使用
一般自己寫的通過路徑來載入,別人寫的通過名稱去當前目錄或全域性的node_modules下面去找
- 第三方模組
- 如果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方法在解析檔名之後,會先檢查模組是否在原生模組列表中。舉個例子,怡http為例,即使在目錄下存在http.js http.json http.node檔案,但require(“http”)不會先從這些檔案中載入,而是優先從原生模組中載入。
從檔案載入
當檔案模組快取中不存在,並且不是原生模組的時候,Node.js會解析require傳入的引數,病載入實際的檔案。
整個檔案模組查詢流程
如果require絕對路徑的檔案,就不會去遍歷每一個node_modules目錄,它的速度最快。其餘流程如下:
- 從module path陣列中取出第一個目錄作為查詢基準。
- 從目錄中查詢該檔案,如果存在,就結束查詢。如果不存在,就進行下一條查詢。
- 通過新增.js .json .node字尾查詢,如果存在檔案就結束查詢。如果不存在,則進行下一條。
- 將require的引數作為一個包進行查詢,讀取目錄下的package.json檔案,取得main(入口檔案)指定的檔案。
- 如果有這個目錄 但是沒有package.json 就會去 當前目錄下查詢index.js index.json ,即重複第3條步驟查詢
- 如果仍沒找到,則取出module path陣列中的下一個目錄作為基準查詢,迴圈1-5條步驟
- 如果繼續失敗,迴圈1-6個步驟,直到module path中的最後一個值。
- 如果仍沒找到就會跑出異常。
整個查詢過程類似原型鏈的查詢和作用域的查詢,但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:Users19624Desktop2018025.modulea
c:Users19624Desktop2018025.modulea
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){","
})"
];
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
複製程式碼