摘要:依賴模塊操作文件的模塊處理路徑的模塊虛擬機(jī),幫我們創(chuàng)建一個黑箱執(zhí)行代碼,防止變量污染創(chuàng)建構(gòu)造函數(shù)其實中引入的每一個模塊我們都需要通過構(gòu)造函數(shù)創(chuàng)建一個實例。
CommonJS 是一種模塊化的標(biāo)準(zhǔn),而 NodeJS 是這種標(biāo)準(zhǔn)的實現(xiàn),每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。
在實現(xiàn)模塊加載之前,我們需要清除模塊的加載過程:
假設(shè) A 文件夾下有一個 a.js,我們要解析出一個絕對路徑來;
我們寫的路徑可能沒有后綴名 .js、.json;
得到一個真實的加載路徑(模塊會被緩存)先去緩存中看一下這個文件是否存在,如果存在返回緩存 沒有則創(chuàng)建一個模塊;
得到對應(yīng)文件的內(nèi)容,加一個閉包,把內(nèi)容塞進(jìn)去,之后執(zhí)行即可。
1、提前加載需要用到的模塊因為我們只是實現(xiàn) CommonJS 的模塊加載方法,并不會去實現(xiàn)整個 Node,在這里我們需要依賴一些 Node 的模塊,所以我們就 “不要臉” 的使用 Node 自帶的 require 方法把模塊加載進(jìn)來。
// 依賴模塊 // 操作文件的模塊 const fs = require("fs"); // 處理路徑的模塊 const path = require("path"); // 虛擬機(jī),幫我們創(chuàng)建一個黑箱執(zhí)行代碼,防止變量污染 const vm = require("vm");2、創(chuàng)建 Module 構(gòu)造函數(shù)
其實 CommonJS 中引入的每一個模塊我們都需要通過 Module 構(gòu)造函數(shù)創(chuàng)建一個實例。
// 創(chuàng)建 Module 構(gòu)造函數(shù) /* * @param {String} p */ function Module(p) { this.id = p; // 當(dāng)前文件的表示(絕對路徑) this.exports = {}; // 每個模塊都有一個 exports 屬性,用來存儲模塊的內(nèi)容 this.loaded = false; // 標(biāo)記是否被加載過 }3、定義靜態(tài)屬性存儲我們需要使用的一些值
// Module 靜態(tài)變量 // 函數(shù)后面需要使用的閉包的字符串 Module.wrapper = [ "(function (exports, require, module, __dirname, __filename) {", " })" ]; // 根據(jù)絕對路徑進(jìn)行緩存的模塊的對象 Module._cacheModule = {}; // 處理不同文件后綴名的方法 Module._extensions = { ".js": function() {}, ".json": function() {} };4、創(chuàng)建引入模塊的 req 方法
為了防止和 Node 自帶的 require 方法重名,我們將模擬的方法重命名為 req。
// 引入模塊方法 req /* * @param {String} moduleId */ function req(moduleId) { // 將 req 傳入的參數(shù)處理成絕對路徑 let p = Module._resolveFileName(moduleId); // 生成一個新的模塊 let module = new Module(p); }
在上面代碼中,我們先把傳入的參數(shù)通過 Module._resolveFileName 處理成了一個絕對路徑,并創(chuàng)建模塊實例把絕對路徑作為參數(shù)傳入,我們現(xiàn)在實現(xiàn)一下 Module._resolveFileName 方法。
5、返回文件絕對路徑 Module._resolveFileName 方法的實現(xiàn)這個方法的功能就是將 req 方法的參數(shù)根據(jù)是否有后綴名兩種方式處理成帶后綴名的文件絕對路徑,如果 req 的參數(shù)沒有后綴名,會去按照 Module._extensions 的鍵的后綴名順序進(jìn)行查找文件,直到找到后綴名對應(yīng)文件的絕對路徑,優(yōu)先 .js,然后是 .json,這里我們只實現(xiàn)這兩種文件類型的處理。
// 處理絕對路徑 _resolveFileName 方法 /* * @param {String} moduleId */ Module._resolveFileName = function(moduleId) { // 將參數(shù)拼接成絕對路徑 let p = path.resolve(moduleId); // 判斷是否含有后綴名 if (!/.w+$/.test(p)) { // 創(chuàng)建規(guī)范規(guī)定查找文件后綴名順序的數(shù)組 .js .json let arr = Object.keys(Module._extensions); // 循環(huán)查找 for (let i = 0; i < arr.length; i++) { // 將絕對路徑與后綴名進(jìn)行拼接 let file = p + arr[i]; // 查找不到文件時捕獲異常 try { // 并通過 fs 模塊同步查找文件的方法對改路徑進(jìn)行查找,文件未找到會直接進(jìn)入 catch 語句 fs.accessSync(file); // 如果找到文件將該文件絕對路徑返回 return file; } catch (e) { // 當(dāng)后綴名循環(huán)完畢都沒有找到對應(yīng)文件時,拋出異常 if (i >= arr.length) throw new Error("not found module"); } } } else { // 有后綴名直接返回該絕對路徑 return p; } };6、加載模塊的 load 方法
// 完善 req 方法 /* * @param {String} moduleId */ function req(moduleId) { // 將 req 傳入的參數(shù)處理成絕對路徑 let p = Module._resolveFileName(moduleId); // 生成一個新的模塊 let module = new Module(p); // ********** 下面為新增代碼 ********** // 加載模塊 let content = module.load(p); // 將加載后返回的內(nèi)容賦值給模塊實例的 exports 屬性上 module.exports = content; // 最后返回 模塊實例的 exports 屬性,即加載模塊的內(nèi)容 return module.exports; // ********** 上面為新增代碼 ********** }
上面代碼實現(xiàn)了一個實例方法 load,傳入文件的絕對路徑,為模塊加載文件的內(nèi)容,在加載后將值存入模塊實例的 exports 屬性上最后返回,其實 req 函數(shù)返回的就是模塊加載回來的內(nèi)容。
// load 方法 // 模塊加載的方法 Module.prototype.load = function(filepath) { // 判斷加載的文件是什么后綴名 let ext = path.extname(filepath); // 根據(jù)不同的后綴名處理文件內(nèi)容,參數(shù)是當(dāng)前實例 let content = Moudule._extensions[ext](this); // 將處理后的結(jié)果返回 return content; };7、實現(xiàn)加載 .js 文件和 .json 文件的方法
還記得前面準(zhǔn)備的靜態(tài)屬性中有 Module._extensions 就是用來存儲這兩個方法的,下面我們來完善這兩個方法。
// 處理后綴名方法的 _extensions 對象 Module._extensions = { ".js": function(module) { // 讀取 js 文件,返回文件的內(nèi)容 let script = fs.readFileSync(module.id, "utf8"); // 給 js 文件的內(nèi)容增加一個閉包環(huán)境 let fn = Module.wrap(script); // 創(chuàng)建虛擬機(jī),將我們創(chuàng)建的 js 函數(shù)執(zhí)行,將 this 指向模塊實例的 exports 屬性 vm.runInThisContext(fn).call( module.exports, module.exports, req, module ); // 返回模塊實例上的 exports 屬性(即模塊的內(nèi)容) return module.exports; }, ".json": function(module) { // .json 文件的處理相對簡單,將讀出的字符串轉(zhuǎn)換成對象即可 return JSON.parse(fs.readFileSync(module.id, "utf8")); } };
我們這里使用了 Module.wrap 方法,代碼如下,其實幫助我們加了一個閉包環(huán)境(即套了一層函數(shù)并傳入了我們需要的參數(shù)),里面所有的變量都是私有的。
// 創(chuàng)建閉包 wrap 方法 Module.wrap = function(content) { return Module.wrapper[0] + content + Module.wrapper[1]; };
Module.wrapper 的兩個值其實就是我們需要在外層包了一個函數(shù)的前半段和后半段。
這里我們要劃重點了,非常重要:
1、我們在虛擬機(jī)中執(zhí)行構(gòu)建的閉包函數(shù)時利用執(zhí)行上/下文 call 將 this 指向了模塊實例的 exports 屬性上,所以這也是為什么我們用 Node 啟動一個 js 文件,打印 this 時,不是全局對象 global,而是一個空對象,這個空對象就是我們的 module.exports,即當(dāng)前模塊實例的 exports 屬性。
2、還是第一條的函數(shù)執(zhí)行,我們傳入的第一個參數(shù)是改變 this 指向,那第二個參數(shù)是 module.exports,所以在每個模塊導(dǎo)出的時候,使用 module.exports = xxx,其實直接替換了模塊實例的值,即直接把模塊的內(nèi)容存放在了模塊實例的 exports 屬性上,而 req 最后返回的就是我們模塊導(dǎo)出的內(nèi)容。
3、第三個參數(shù)之所以傳入 req 是因為我們還可能在一個模塊中導(dǎo)入其他模塊,而 req 會返回其他模塊的導(dǎo)出在當(dāng)前模塊使用,這樣整個 CommonJS 的規(guī)則就這樣建立起來了。
我們現(xiàn)在的程序是有問題的,當(dāng)重復(fù)加載了一個已經(jīng)加載過得模塊,當(dāng)執(zhí)行 req 方法的時候會發(fā)現(xiàn),又創(chuàng)建了一個新的模塊實例,這是不合理的,所以我們下面來實現(xiàn)一下緩存機(jī)制。
還記得之前的一個靜態(tài)屬性 Module._cacheModule,它的值是一個空對象,我們會把所有加載過的模塊的實例存儲到這個對象上。
// 完善 req 方法(處理緩存) /* * @param {String} moduleId */ function req(moduleId) { // 將 req 傳入的參數(shù)處理成絕對路徑 let p = Module._resolveFileName(moduleId); // ********** 下面為新增代碼 ********** // 判斷是否已經(jīng)加載過 if (Module._cacheModule[p]) { // 模塊存在,如果有直接把 exports 對象返回即可 return Module._cacheModule[p].exprots; } // ********** 上面為新增代碼 ********** // 生成一個新的模塊 let module = new Module(p); // 加載模塊 let content = module.load(p); // ********** 下面為新增代碼 ********** // 存儲時是拿模塊的絕對路徑作為鍵與模塊內(nèi)容相對應(yīng)的 Module._cacheModule[p] = module; // 是否緩存表示改為 true module.loaded = true; // ********** 上面為新增代碼 ********** // 將加載后返回的內(nèi)容賦值給模塊實例的 exports 屬性上 module.exports = content; // 最后返回 模塊實例的 exports 屬性,即加載模塊的內(nèi)容 return module.exports; }9、試用 req 加載模塊
在同級目錄下新建一個文件 a.js,使用 module.exports 隨便導(dǎo)出一些內(nèi)容,在我們實現(xiàn)模塊加載的最下方嘗試引入并打印內(nèi)容。
// 導(dǎo)出自定義模塊 // a.js module.exports = "Hello world";
// 檢測 req 方法 const a = req("./a"); console.log(a); // Hello world
其實我們只實現(xiàn)了 CommonJS 規(guī)范的一部分,即自定義模塊的加載,其實在 CommonJS 的規(guī)范當(dāng)中關(guān)于模塊查找的規(guī)則還有很多,具體的我們就用下面的流程圖來表示。
這篇文章讓我們了解了 CommonJS 是什么,主要目的在于理解 Node 模塊化的實現(xiàn)思路,想要更深入的了解 CommonJS 的實現(xiàn)細(xì)節(jié),建議看一看 NodeJS 源碼對應(yīng)的部分,如果覺得源碼比較多,不容易找到模塊化實現(xiàn)的代碼,也可以在 VSCode 中通過調(diào)用 require 方法引入模塊時,打斷點調(diào)試,一步一步的跟進(jìn)到 Node 源碼中查看。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/98306.html
摘要:所有依賴這個模塊的語句,都定義在一個回調(diào)函數(shù)中,等到加載完成之后,這個回調(diào)函數(shù)才會運(yùn)行。也采用語句加載模塊,但是不同于,它要求兩個參數(shù)第一個參數(shù),是一個數(shù)組,里面的成員就是要加載的模塊第二個參數(shù),則是加載成功之后的回調(diào)函數(shù)。 本篇文章來自對文章《js模塊化編程之徹底弄懂CommonJS和AMD/CMD!》的總結(jié),大部分摘自文章原話,本人只是為了學(xué)習(xí)方便做的筆記,之后有新的體會會及時補(bǔ)充...
摘要:簡易模塊加載器示例點來了接下來我們先來看一段建議模塊加載器的示例代碼以上是加載器的實現(xiàn),再來看看如何使用吧你好啊要去杭州玩了章煒今天天氣不錯噢在以上代碼中,我們定義了三個模塊,分別名為。 前端模塊化 關(guān)注前端技術(shù)發(fā)展的各位親們,肯定對模塊化開發(fā)這個名詞不陌生。隨著前端工程越來越復(fù)雜,代碼越來越多,模塊化成了必不可免的趨勢。 各種標(biāo)準(zhǔn) 由于javascript本身并沒有制定相關(guān)標(biāo)準(zhǔn)(當(dāng)然...
摘要:而在編譯過程中通過語法和詞法的分析得出一顆語法樹,我們可以將它稱為抽象語法樹也稱為語法樹,指的是源代碼語法所對應(yīng)的樹狀結(jié)構(gòu)。而這個卻恰恰使我們分析打包工具的重點核心。 概述 眼下wepack似乎已經(jīng)成了前端開發(fā)中不可缺少的工具之一,而他的一切皆模塊的思想隨著webpack版本不斷的迭代(webpack 4)使其打包速度更快,效率更高的為我們的前端工程化服務(wù)showImg(https:/...
摘要:首先一段代碼轉(zhuǎn)化成的抽象語法樹是一個對象,該對象會有一個頂級的屬性第二個屬性是是一個數(shù)組。最終完成整個文件依賴的處理。參考文章抽象語法樹一看就懂的抽象語法樹源碼所有的源碼已經(jīng)上傳 背景 隨著前端復(fù)雜度的不斷提升,誕生出很多打包工具,比如最先的grunt,gulp。到后來的webpack和 Parcel。但是目前很多腳手架工具,比如vue-cli已經(jīng)幫我們集成了一些構(gòu)建工具的使用。有的時...
閱讀 1711·2021-11-11 10:58
閱讀 4184·2021-09-09 09:33
閱讀 1257·2021-08-18 10:23
閱讀 1548·2019-08-30 15:52
閱讀 1624·2019-08-30 11:06
閱讀 1867·2019-08-29 14:03
閱讀 1507·2019-08-26 14:06
閱讀 2943·2019-08-26 10:39