目錄
CommonJS
CommonJS是一個模組化的規範,Nodejs的模組系統,就是參照CommonJS規範實現的。每個檔案就是一個模組 ,每個模組都要自己的作用域。
特點
- 所有程式碼都有執行在模組作用域,不會汙染全域性作用域
- 模組可以多次載入,但是隻會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。要想讓模組再次執行,必須清除快取。
- 模組載入的順序,按照其在程式碼中出現的順序。
NodeJs 模組化簡單實現
首先我們先了解一下模組化載入:
- 模組載入
- 模組解析檔名,解析出絕對路徑來
- 我們引入時可以省略檔案的字尾,有可能沒有寫字尾名, 會自動查詢對應目錄下
.js
或.json
的檔案 - 多次呼叫只走一次, 得到一個真實存在的檔案路徑,在快取中看檔案是否存在,不存在則建立模組,然後放入到快取中,方便別人讀取
- 讀取檔案內容。如果是
.js
檔案,就把內容加一個閉包 - 執行JS指令碼,將結果返回
0. 建立module建構函式
每個模組內部,都有一個 module
物件,代表當前模組。
檢視 console.log(module)
module都有哪些屬性:
module.id
模組的識別符,通常是帶有絕對路徑的模組檔名module.filename
模組的檔名,帶有絕對路徑module.loaded
返回一個布林值,表示模組是否載入module.parent
返回一個物件,表示呼叫該模組的模組module.children
返回一個陣列,表示該模組要用到的其他模組module.exports
表示模組對外輸出的值,預設值為空物件
function Module(id) {
// 下面就簡單定義兩個常用的
this.id = id
this.exports = {}
}
複製程式碼
1. 模組載入
引入自己寫的模組
// 我們自己寫的檔案模組 要寫路徑 ./ 或者../
let hello = require('./1.NodeJS')
console.log(hello) // first NodeJS
複製程式碼
引入內建模組就直接寫名稱即可
//操作檔案的模組
let fs = require('fs')
// 處理路徑的模組
let path = require('path')
// 虛擬機器模組,沙箱執行,防止變數汙染
let vm = require('vm')
複製程式碼
2. 解析檔案,返回絕對路徑
Module._resolveFilename
方法實現的功能是判斷使用者引入的模組是否包含字尾名,如果沒有字尾名會根據 Module._extensions
的鍵的順序查詢檔案,直到找到字尾名對應的檔案的絕對路徑,優先查詢 .js
// 將引入檔案處理為絕對路徑
Module._resolveFilename = function (p) {
// 以js或者json結尾的
if ((/\.js$|\.json$/).test(p)) {
// __dirname當前檔案所在的資料夾的絕對路徑
// path.resolve方法就是幫我們解析出一個絕對路徑出來
return path.resolve(__dirname, p);
} else {
// 沒有後字尾 自動拼字尾
// Module._extensions 處理不同字尾的模組
let exts = Object.keys(Module._extensions);
let realPath; // 存放真實存在檔案的絕對路徑
for (let i = 0; i < exts.length; i++) {
// 依次匹配對應副檔名的絕對路徑
let temp = path.resolve(__dirname, p + exts[i])
try {
// 通過fs的accessSync方法對路徑進行查詢,找不到對應檔案直接報錯
fs.accessSync(temp)
realPath = temp
break
} catch (e) {
}
}
if (!realPath) {
throw new Error('module not exists');
}
// 將存在絕對路徑返回
return realPath
}
}
複製程式碼
Module._extensions
處理對應模組副檔名。這裡我們只提 .js
.json
,對應模組的處理功能我們在後面來實現
Module._extensions = {
"js": function() {},
"json": function() {}
}
複製程式碼
3. 多次呼叫只一次
當使用者重複載入一個已經載入過的模組,我們只有第一次是載入,然後放入快取中,後面在載入時,直接返回快取中即可
Module._cacheModule = { }
存放模組快取
// 獲取記憶體中的結果
let cache = Module_cacheModule[filename]
// 判斷是否存在
if (cache) {
// 如果存在直接將 exports物件返回
return cache.exports
}
// 如果不存在記憶體中,就建立模組,然後加入到記憶體中
let module = new Module(filename)
Module._cacheModule[filename] = module
複製程式碼
4. 載入模組,對於不同型別的檔案做不同的處理
根據前面我們說到的模組化,對於已經存放在記憶體中的我們直接返回就可以了,對於新新增的模組,我們該讀取檔案了, 根據傳入的模組,嘗試載入模組方法
// 根據傳入的模組,嘗試載入模組方法
function tryModuleLoad(module) {
// 前面我們已經提到 module.id 為模組的識別符,通常是帶有絕對路徑的模組檔名
// path.extname 獲取檔案的副檔名
/* let ext = path.extname(module.id);
// 如果副檔名是js 呼叫js處理器 如果是json 呼叫json處理器
Module._extensions[ext](module); // exports 上就有了陣列 */
let ext = path.extname(module.id);//副檔名
// 如果副檔名是js 呼叫js處理器 如果是json 呼叫json處理器
Module._extensions[ext](module); // exports 上就有了陣列
}
複製程式碼
Module._extensions
處理對應字尾名模組。這裡我們只提 .js
.json
// 處理對應字尾名模組
Module._extensions = {
".js": function (module) {
// 對於js檔案,讀取內容
let content = fs.readFileSync(module.id, 'utf8')
// 給內容新增閉包, 後面實現
let funcStr = Module.wrap(content)
// vm沙箱執行, node內建模組,前面我們已經引入, 將我們js函式執行,將this指向 module.exports
vm.runInThisContext(funcStr).call(module.exports, module.exports, req, module)
},
".json": function (module) {
// 對於json檔案的處理就相對簡單了,將讀取出來的字串轉換未JSON物件就可以了
module.exports = JSON.parse(fs.readFileSync(module.id, 'utf8'))
}
}
複製程式碼
上面我們使用了 Module.wrap
方法,是幫助我們新增一個閉包,簡單說就是我們在外面包了一個函式的前半段和後半段
// 存放閉包字串
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
複製程式碼
// 將我們讀到js的內容傳入,組合成閉包字串
Module.wrap = function (script) {
return Module.wrapper[0] + script + Module.wrapper[1];
}
複製程式碼
5. 執行並返回結果
要執行,我們來看一下完整的模組載入程式碼
// 模組載入
Module._load = function (f) {
// 相對路徑,可能這個檔案沒有字尾,嘗試加字尾
let fileName = Module._resolveFilename(f); // 獲取到絕對路徑
// 判斷快取中是否有該模組
if (Module._cacheModule[fileName]) {
return Module._cacheModule[fileName].exports
}
let module = new Module(fileName); // 沒有就建立模組
Module._cacheModule[fileName] = module // 並將建立的模組新增到快取
// 載入模組
tryModuleLoad(module)
return module.exports
}
複製程式碼
到這裡,一個簡單的module就實現了,讓我們測試一下吧
// 測試程式碼
function req(p) {
return Module._load(p); // 載入模組
}
// 第一次沒有快取,建立module,並新增到快取中
let str = req('./1.NodeJS');
// 第二次就是返回的快取中的
let str1 = req('./1.NodeJS.js');
console.log(str) // first NodeJS
console.log(str1) // first NodeJS
複製程式碼
附原始碼
重要:為了方便大家瞭解、檢視、除錯程式碼,完整的原始碼參見gitHub
總結
本篇文章是基於CommonJS規範,實現了一個簡單的NodeJS模組化,主要目的在於理解 NodeJS 模組化的實現思路,希望對大家瞭解模組化起到一定的作用。
作者:香香
將來的你,一定會感謝現在拼命努力的自己!