node採用的是CommonJS規範。每個文件就是一個單獨的模塊,擁有屬於自身的獨立做用域,變量以及方法等。這些對其餘模塊都是不可見的。CommonJS規範規定,每一個模塊內部,module表明當前模塊。module是一個對象,它有一個exports屬性,也就是module.exports。該屬性是對外的接口,把須要導出的內容放到該屬性上。外部能夠經過require進行導入。require導入的就是exports中的內容。node
該篇文章就手動實現如下require方法,經過手寫的require方法拿到另外一個文件中的exports中的內容。json
首先,咱們先看一下node環境中標準的require方法是如何引用模塊的。
新建文件夾,在文件夾中新建b.js。經過module.exports將內容導出。
b.js:瀏覽器
let str = 'b.js導出的內容'; module.exports = str;
而後新建另外一個文件,my-require.js。在my-require.js中引入b.js中的str。
my-require.js:安全
let str = require('./b.js'); console.log(str);
運行代碼,能夠看到。打印出了b的內容:b.js導出的內容。
以上是標準CommonJS中require的引用,接下來手動實現它:
首先梳理如下邏輯,require函數中傳遞的參數是一個路徑,有路徑再加上node的fs模塊,咱們就能夠讀取到該文件。那有了該文件的內容,從該文件中獲取exports就不是什麼難事了。
上代碼:app
let path = require('path'); let fs = require('fs'); let vm = require('vm'); /** 定義本身的require方法 myrequire() */ function myrequire(modulePath){ let absPath = path.resolve(__dirname,modulePath); function find(absPath){ try{ fs.accessSync(absPath); return absPath; }catch(e){ console.log(e); } } absPath = find(absPath); let module = new Module(absPath); loadModule(module); return module.exports; } function Module(id){ this.id = id; this.exports = {} } function loadModule(module){ let extension = path.extname(module.id); Module._extensions[extension](module); } Module._extensions = { '.js'(module){ let content = fs.readFileSync(module.id, 'utf8'); let fnStr = Module.wrapper[0]+content+Module.wrapper[1]; let fn = vm.runInThisContext(fnStr); fn.call(module.exports,module.exports,module,myrequire); } } Module.wrapper = [ '(function(exports,module,require,__dirname,__dirname){', '})' ]; let str = myrequire('./b.js'); console.log(str);
閱讀順序從上至下。首先 引入了path fs和vm模塊。path和fs都不用說了,都懂。vm模塊是node的核心模塊。核心功能官方解釋的是:ide
意思大體是:vm能夠使用v8的Virtual Machine contexts動態地編譯和執行代碼,而代碼的執行上下文是與當前進程隔離的,可是這裏的隔離並非絕對的安全,不徹底等同瀏覽器的沙箱環境。
其實vm模塊在該本文中的做用就是執行字符串代碼,這樣理解就好。函數
首先,定義了一個myrequire的方法。該方法傳入一個相對路徑。在myrequire方法中第一步將相對路徑轉換爲絕對路徑。而後又經過一個find方法來校驗該路徑是否存在。接下來經過構造函數Module傳入絕對路徑,new出了實例module。
該構造函數Module傳入了路徑id,內部定義了屬性exports={}。該屬性就是文件導出的屬性。ui
緊接着,經過loadModule方法傳入了實例module,來加載該文件。在loadModule方法中,首先獲取了文件名後綴.js。 把文件名後綴.js傳給Module._extensions。在Module._extensions對象中,經過文件後綴名.js找到該文件類型的解析方法。並把實例module傳遞進去。
在該方法中,經過module.id路徑和fs模塊經過獲取到該文件內容content。注意下一步。在該文件內容content的外面用(function(exports,modules,require,__dirname,__filename){})函數包裹了一層。這樣作的目的是待會要執行該函數而且拿到其中的module.exports中導出的內容。可是咱們剛纔經過fs讀取到的文件內容僅僅是字符串,又包裹了一層空函數,仍是字符串。
接下來就要用到vm模塊。該模塊能夠執行字符串代碼。經過vm.runInthisContext()方法,將剛纔獲得的字符串傳遞進去。此時就獲得了能夠執行的方法fn。
那接下來就是執行該方法fn了。執行fn,把剛纔的參數傳遞進去。注意當前this執行爲module.exports。這樣才能拿到module.exports中的內容。
最後在myrequire中末尾,返回了該exports內容。return module.exports。
好,接下來就是驗證效果了。右鍵code run,或者瀏覽器中打開。能夠看到:this
b.js導出的內容
拿到了文件b.js中的內容,而且打印了出來。
好,如今以及實現了最簡單了require。但是,咱們並不知足於此。由於該require方法還有一些問題。好比說,還不能引用json文件,並且也沒有考慮若是文件沒有後綴的狀況。接下來繼續完善myrequire方法:code
let path = require('path'); let fs = require('fs'); let vm = require('vm'); /** 定義本身的require方法 myrequire() */ function myrequire(modulePath){ let absPath = path.resolve(__dirname,modulePath); let ext_name = Object.keys(Module._extensions); let index = 0; let old_absPath = absPath; function find(absPath){ try{ fs.accessSync(absPath); return absPath; }catch(e){ let ext = ext_name[index++]; let newPath = old_absPath+ext; return find(newPath); } } absPath = find(absPath); let module = new Module(absPath); loadModule(module); return module.exports; } function Module(id){ this.id = id; this.exports = {} } function loadModule(module){ let extension = path.extname(module.id); Module._extensions[extension](module); } Module._extensions = { '.js'(module){ let content = fs.readFileSync(module.id, 'utf8'); let fnStr = Module.wrapper[0]+content+Module.wrapper[1]; let fn = vm.runInThisContext(fnStr); fn.call(module.exports,module.exports,module,myrequire); }, '.json'(module){ let content = fs.readFileSync(module.id, 'utf8'); module.exports = content; } } Module.wrapper = [ '(function(exports,module,require,__dirname,__dirname){', '})' ]; let str = myrequire('./b'); console.log(str); console.log(myrequire('./a'));
在myrequire方法的第二行,先獲取到Module._extensions中的全部後綴(目前有.js和.json),又聲明瞭一個下標index,最後有保存了該路徑old_absPath。 在find方法中,若是用戶沒有寫文件後綴,就會自動拼接後綴。循環去查找,直到找到或者到最後也沒找到。
在Module._extensions中新增了一個對象.json的方法。該方法較爲簡單。經過fs讀取到文件並把文件內容放到module.exports中。ok,看下效果吧:
b.js導出的內容 { "name":"要引入的內容" }
能夠看到。正常拿到了b.js中的內容並且也讀取到了a.json中的內容。至此,咱們就實現了CommonJS中的require方法。寫文章不易,喜歡就點個👍吧 thx~