簡單的模塊載入器

簡單的模塊載入器

來自專欄 那些年我不清楚的問題

Node 有個 vm 模塊,其中 vm 下有個 runInThisContext 方法很有意思,此方法可以編譯運行字元串格式的 JS 代碼,這點像 JS 的 eval 方法,但又不完全相同。下面代碼可以看出二者之間的差異:

// evallet name = weixian;eval(`(function() {console.log(name)})`)(); // weixian

// vm.runInThisContext// 運行中的代碼無法獲取本地作用域,但可以獲取當前的global對象let vm = require(vm);let name = "weixian";vm.runInThisContext(`(function() {console.log(name)})`)(); // name is not defined

本文目的:根據 vm.runInThisContext 的特性(函數里訪問不到外面的),配合匿名函數作用域(外面的訪問不到函數里的),實現一個簡單的模塊載入器,模塊載入器的大概功能如下:

  • load(./a.json) 載入當前目錄下的 a.json 文件
  • load(./a.js) 載入當前目錄下的 a.js 文件
  • load(./a) 優先載入當前目錄下的 a.js ,不存在就找 a.json ,再不存在就報錯
  • 若重複多次 load(./a) ,保證只載入一次,避免重複載入浪費資源

第一步:解析路徑

解析路徑的意思是:如果 load(./a) ,自動去找 load(./a.js),找不到則找 load(./a.json)。

// index.jslet fs = require(fs), pathSystem = require(path);function resolvePath(path) { // 轉為絕對路徑 path = pathSystem.resolve(path); if (pathSystem.extname(path)) { // 有後綴直接返回 return path; } else { // 無後綴分別添加.js和.json後綴,看是否存在此文件 let arrKey = [.js, .json]; let iNum = 0; for (let i = 0, len = arrKey.length; i < len; i++) { let fullPath = path + arrKey[i]; try { // 是否存在路徑,存在不報錯,反之報錯 fs.accessSync(fullPath); return fullPath; } catch (e) { if (++iNum === arrKey.length) { // 說明每次都沒找到,那麼是時候列印出錯誤了 console.log(e); } } } }};function load(path) { // 解析路徑:絕對路徑和後綴名問題 path = resolvePath(path); console.log(path); // a.json}load(./a);

第二步:解析JSON文件

a.json

{ "name": "weixian"}

// index.jsfunction load(path) { // 解析路徑:絕對路徑和後綴名問題 path = resolvePath(path); // 根據解析過的文件後綴,如果是.json,讀出來返回即可 if(pathSystem.extname(path) === .json) { return JSON.parse(fs.readFileSync(path, "utf-8")); }}let obj = load(./a);console.log(obj); // {"name": "weixian"}

第三步:解析JS文件

// b.jsmodule.exports = function() { console.log(hello world);};

// index.jsfunction load(path) { // 解析路徑:絕對路徑和後綴名問題 path = resolvePath(path); // 存放讀取到文件的內容 let module = { exports: null }; if (pathSystem.extname(path) === .js) { // 把讀取到的函數包在匿名函數里,這樣外部就訪問不到函數里的變數 #1 let fnTxt = (function(module){ + fs.readFileSync(path, "utf-8") + }); // 傳進去 module,會被函數b.js里的 module.exports 改變 // 通過 runInThisContext 而不是eval,這樣函數里就訪問不到外面定義的變數 #2 // #1 和 #2 對比理解下就明白啦 vm.runInThisContext(fnTxt)(module); } return module.exports;}let fn = load(./b);fn(); // hello world

第四步:防止重複載入

// b.jsconsole.log(I am b.js ~);module.exports = function() { };

如果像下面這樣寫法實際是會載入兩遍,這並不是我們所希望的

// index.jsload(./b); // I am b.js ~load(./b); // I am b.js ~

解決的辦法是載入第一次的時候根據路徑存起來(路徑是唯一的),再載入如果有就直接返回

// index.js;(function() { // 存放讀取到文件的內容 let module = { exports: null }; function load(path) { path = resolvePath(path); // 如果有了,就直接返回 if (module[path]) return module[path]; if (pathSystem.extname(path) === .js) { let fnTxt = (function(module){ + fs.readFileSync(path, "utf-8") + }); vm.runInThisContext(fnTxt)(module); } // 存起來 module[path] = module.exports; return module.exports; } load(./b)(); // I am b.js~ load(./b)(); // 不會重新載入,取的是存起來的那個函數})();

第五步:規整下代碼

let fs = require(fs), pathSystem = require(path), vm = require(vm);// 模塊載入器的實現function Module(path) { this.path = path; this.exports = {};}Module.prototype.read = function (path) { let ext = pathSystem.extname(path); // 調用支持後綴的對應的讀取方法 Module._support[ext](this);};// 支持類型Module._support = { ".js": function (module) { let fnTxt = (function(module){ + fs.readFileSync(module.path, "utf-8") + }); // 函數里 module.exports 會改變module vm.runInThisContext(fnTxt).call(module.exports, module); }, ".json": function (module) { return JSON.parse(fs.readFileSync(module.path, "utf-8")); }};// 解析路徑Module._resolvePath = function (path) { path = pathSystem.resolve(path); if (pathSystem.extname(path)) { // 有後綴 return path; } else { // 無後綴 let arrKey = Object.keys(Module._support); let iNum = 0; for (let i = 0, len = arrKey.length; i < len; i++) { let fullPath = path + arrKey[i]; try { // 是否存在路徑,存在不報錯,反之報錯 fs.accessSync(fullPath); return fullPath; } catch (e) { if (++iNum === arrKey.length) { // 說明每次都沒找到則列印出錯誤 console.log(e); } } } }};// 緩存module.exportsModule._cache = {};function load(path) { // 解析路徑:絕對路徑和後綴名問題 path = Module._resolvePath(path); // 有則返回,無則繼續 if (Module._cache[path]) { return Module._cache[path]; } // 讀取文件並返回讀取內容 let module = new Module(path); module.read(path); // 存 Module._cache[path] = module.exports; return module.exports;}load(./a)();

題圖來源:Photo by Jakob Owens on Unsplash

推薦閱讀:

隔離電源與非隔離電源的優缺點
雲資料庫Redis熱點Key的發現與解決之道
當資料庫遇見FPGA:X-DB異構計算如何實現百萬級TPS?
光纖跳線詳解
Nginx學習之配置RTMP模塊搭建推流服務

TAG:前端開發 | 模塊 |