你對CommonJS規範瞭解多少?

寫在前面

爲何會出現CommonJS規範?

由於JavaScript自己並無模塊的概念,不支持封閉的做用域和依賴管理,傳統的文件引入方式又會污染變量,甚至文件引入的前後順序都會影響整個項目的運行。同時也沒有一個相對標準的文件引入規範和包管理系統,這個時候CommonJS規範就出現了。node

CommonJS規範的優勢有哪些?

  • 首先要說的就是它的封裝功能,模塊化能夠隱藏私有的屬性和方法,這樣不須要別人在從新造輪子。
  • 第二就是它可以封裝做用域,保證了命名空間不會出現命名衝突的問題。
  • 第三nodejs中npm包管理有20萬以上的包而且被全球的開發人員不斷更新維護,開發效率幾何倍增。

模塊化的定義

下面就是本文的重頭戲部分了,經過手寫一個CommonJS規範,更加清晰和認識模塊化的含義及如何實現的。另外本文中的示例代碼須要在node.js環境中方可正常運行,不然將出現錯誤。事實上ES6已經出現了模塊規範,若是使用ES6的模塊規範是無需node.js環境的。所以,須要將commonJS規範和ES6的模塊規範區分開來。正則表達式

1.自執行函數

咱們先寫一段簡單的代碼,在node環境下運行,來看看commonJS是如何處理的:npm

一段很是簡單的函數,調用時候傳遞參數name,將一段字符串返回。可是經過斷點調試咱們發如今node環境下,node自己自動給sayHello函數加了一層外衣,就是下面的內容:

(function (exports, require, module, __filename, __dirname) {});
複製代碼

咱們不難發現,其實這是一個自執行函數,那麼爲何要加上這樣一段看似多餘的代碼吶,這就是咱們說得CommonJS規範一個好處,它將要執行的函數封裝了起來,全部的變量和方法均可以理解爲是私有的了,保證了命名空間。json

2.文件導出

前面咱們已經瞭解到在node中,每一個文件均可以被當作是一個模塊,那麼node中對於模塊的導出,都是使用的相同的方法module.exports。緩存

var str='hello World';
module.exports=str;
複製代碼

####3.文件導入 爲了方便的使用模塊,咱們可使用require方法對模塊進行導入,相似於這樣:bash

var a=require('./a.js');
複製代碼

值的注意的是:在文件引入的過程當中,是否使用相對或者絕對路徑,若是a.js前添加./或者../是證實是第三方模塊,不寫絕對和相對路徑爲內置模塊,例如:fsapp

分析commonJS規範源碼

咱們寫一個簡單的模塊引入,經過斷點,分析它的代碼,並以此爲來完善咱們本身的commonJS規範模塊化

Module._load

首先咱們能看到第一次進入是require方法中,分析代碼:

  • assert方法用來進行斷言,那麼第一行代碼的含義就是判斷一下這個路徑的參數path是否存在,若是不存在就報錯
  • 同理第二行代碼檢查路徑參數是否是一個字符串格式,若是不是也報錯
  • 第三返回一個函數Module._load,從名字中能夠看出這應該是一個加載的方法,此方法傳遞三個參數,第一個是路徑,第二個是this的指向,第三個是一個布爾值,表示爲是否爲必要的。

Module._resolveFilename

斷點繼續運行,走到下一個方法Module._resolveFilename,這個方法是用來解析文件名稱的,將相對路徑解析成絕對路徑。函數

var filename = Module._resolveFilename(request, parent, isMain);
複製代碼

Module._cache

node中會對已經加載過的模塊進行緩存,供下次引入時候使用,這個方法就是:Module._cacheui

var cachedModule = Module._cache[filename];
複製代碼

new modal

沒有緩存的時候,node會新建一個模塊,用來存放這個正在加載的模塊:

var module = new Module(filename, parent);
Module._cache[filename] = module;
複製代碼

tryModuleLoad

而後嘗試加載這個模塊

tryModuleLoad(module, filename);
複製代碼

Module._extensions

而後繼續回到load方法中,執行下面的代碼,對擴展名進行完善:

var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
複製代碼

Module.wrap

有了文件名以後就就能夠拿到對應的文件內容,下面就對文件內容進行處理,咱們稱這個方法爲文件包裹方法:

var wrapper = Module.wrap(content);
複製代碼

進入這個方法以後你會看到咱們熟悉的自執行函數,經過字符串拼接的形式進行包裹。

而後讓這個函數執行

手寫commonJS規範

初始化

首先得有一個方法或者類實現這樣一個規範,而後這個方法接受一個參數path(路徑)

let fs = require('fs');//文件模塊,用來讀取文件
let path = require('path');//用來完善文件路徑
let vm=require('vm');//將字符串看成JavaScript執行
function req(path) { 

}
function module() { //模塊相關

}
複製代碼

Module._load

第一步加載,傳入參數路徑,進入到方法中會有一個Module._resolveFilename,用來解析文件名,咱們的代碼就變成了:

let fs = require('fs');//文件模塊,用來讀取文件
let path = require('path');//用來完善文件路徑
let vm=require('vm');//將字符串看成JavaScript執行
function req(path) { 
    module._load(path);//嘗試加載模塊
}
function module() { //模塊相關

}
module._load = function (path) { //
    let fileName=module._resolveFilename(path)//解析文件名
}
module._resolveFilename = function (path) { 

}
複製代碼

在進入這個_resolveFilename方法的時候,傳入的參數可能沒有後綴,多是一個相對路徑,繼續完善module._resolveFilename方法:

module._resolveFilename

咱們利用正則表達式來對文件名後綴進行分析,這裏只考慮是js文件仍是json文件,而後利用path模塊完善文件後綴

module._resolveFilename = function (p) { 
    if ((/\.js$|\.json$/).test(p)) { 
        // 以js或者json結尾的
        return path.resolve(__dirname, p);
    }else{
        // 沒有後後綴  自動拼後綴
    }
}
複製代碼

若是沒有文件後綴名,咱們須要補全後綴名,就調用了Module._extensions

Module._extensions

module._extensions = {
    '.js':function (module) {},
    '.json':function (module) {}
  }
複製代碼

module._resolveFilename方法中對_extensions這個對象進行遍歷,而後將後綴名加上繼續嘗試,而後經過fs模塊的accessSync方法對拼接好的路徑進行判斷,代碼以下:

Module._resolveFilename = function (p) {
   if((/\.js$|\.json$/).test(p)){
     // 以js或者json結尾的
     return path.resolve(__dirname, p);
   }else{
    // 沒有後後綴  自動拼後綴
     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(temp); // 存在的 
           realPath = temp
           break; 
         } catch (e) {
         }
       }
       if(!realPath){
        throw new Error('module not exists');
       }
       return realPath
   }
}
複製代碼

到如今咱們已經能夠拿到完整的絕對路徑和後綴名了,根據上面的分析,咱們就要去緩存中查看是否有緩存,若是有,就是用緩存的,若是沒有,加入緩存中。

Module._cache

首先去Module._cache這個對象中查找是否有,若是有就直接返回模塊中的exports,也就是cache.exports,若是沒有,就新建立一個模塊。並將模塊的絕對路徑做爲module的id屬性

Module._cache = {};
Module._load = function (p) { // 相對路徑,可能這個文件沒有後綴,嘗試加後綴
  let filename = Module._resolveFilename(p); // 獲取到絕對路徑
  let cache = Module._cache[filename];
  if(cache){ // 第一次沒有緩存 不會進來
    
  }
  let module = new Module(filename); // 沒有模塊就建立模塊
  Module._cache[filename] = module;// 每一個模塊都有exports對象 {}

  //嘗試加載模塊
  tryModuleLoad(module);
  return module.exports
}
複製代碼

下面就開始嘗試加載這個模塊,並將module.exports返回。

tryModuleLoad

經過模塊的id咱們能夠很方便的拿到文件的擴展名,而後利用path.extname方法來獲取文件的擴展名,並調用對應擴展名下面的處理方法:

function tryModuleLoad(module){
  let ext = path.extname(module.id);//擴展名
  // 若是擴展名是js,調用js處理器.若是是json,調用json處理器
  Module._extensions[ext](module);
}
複製代碼

完善Module._extensions

若是這個文件是一個json文件。由於讀文件返回的是一個字符串,因此要用JSON.parse轉換讀到的文件,至此對於json文件的引入就所有搞定了,因此要將module.exports賦值,這樣外面return纔有內容。 若是是一個js文件,用獲取到的絕對路徑也就是 module的id屬性進行文件讀取,而後調用Module.wrap對文件內容進行包裹,也就是加在對應的自執行函數,而後執行這個函數。 Module._extensions完善以下:

Module._extensions = {
  '.js':function (module) {
    let content = fs.readFileSync(module.id, 'utf8');
    let funcStr = Module.wrap(content);
    let fn = vm.runInThisContext(funcStr);
    fn.call(module.exports,module.exports,req,module);
  },
  '.json':function (module) {
    module.exports = JSON.parse(fs.readFileSync(module.id, 'utf8'));
  }
}
複製代碼

Module.wrap

咱們用倆個字符串將文件內容進行包裹並返回新的字符串

Module.wrapper = [
  "(function (exports, require, module, __filename, __dirname) {",
  "})"
]
Module.wrap = function (script) {
  return Module.wrapper[0] + script+ Module.wrapper[1];
}
複製代碼

小細節處理

到如今咱們的代碼已經基本完成了,可是如今出現的問題是每次require的代碼都會被執行,咱們但願的是有這個模塊的時候要直接使用exports中的值,因此代碼能夠這樣完善:

if(cache){ // 第一次沒有緩存 不會進來
    return cache.exports;
  }
複製代碼

寫在最後

上面的代碼不少狀況的處理我並無給出,好比path的處理等等。和真正的commonJS規範代碼仍是有不少不足的地方,可是我但願經過這樣的方式能夠加深你對commonJS規範的理解和使用,特此說明。

相關文章
相關標籤/搜索