深刻理解CommonJS規範
node採用的是CommonJS規範。每一個檔案就是一個單獨的模組,擁有屬於自身的獨立作用域,變數以及方法等。這些對其他模組都是不可見的。CommonJS規範規定,每個模組內部,module代表當前模組。module是一個物件,它有一個exports屬性,也就是module.exports。該屬性是對外的介面,把需要匯出的內容放到該屬性上。外部可以通過require進行匯入。require匯入的就是exports中的內容。
該篇文章就手動實現以下require方法,通過手寫的require方法拿到另一個檔案中的exports中的內容。
首先,我們先看一下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就不是什麼難事了。
上程式碼:
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的核心模組。核心功能官方解釋的是:
- The vm module provides APIs for compiling and running code within V8 Virtual Machine contexts. The vm module is not a security mechanism. Do not use it to run untrusted code. The term "sandbox" is used throughout these docs simply to refer to a separate context, and does not confer any security guarantees.
意思大致是:vm可以使用v8的Virtual Machine contexts動態地編譯和執行程式碼,而程式碼的執行上下文是與當前程序隔離的,但是這裡的隔離並不是絕對的安全,不完全等同瀏覽器的沙箱環境。
其實vm模組在該本文中的作用就是執行字串程式碼,這樣理解就好。
首先,定義了一個myrequire的方法。該方法傳入一個相對路徑。在myrequire方法中第一步將相對路徑轉換為絕對路徑。然後又通過一個find方法來校驗該路徑是否存在。接下來通過建構函式Module傳入絕對路徑,new出了例項module。
該建構函式Module傳入了路徑id,內部定義了屬性exports={}。該屬性就是檔案匯出的屬性。
緊接著,通過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,或者瀏覽器中開啟。可以看到:
b.js匯出的內容
拿到了檔案b.js中的內容,並且列印了出來。
好,現在以及實現了最簡單了require。可是,我們並不滿足於此。因為該require方法還有一些問題。比如說,還不能引用json檔案,而且也沒有考慮如果檔案沒有後綴的情況。接下來繼續完善myrequire方法:
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方法。寫文章不易,喜歡就點個:+1:吧 thx~