你對CommonJS規範瞭解多少?

離秋發表於2018-09-10

寫在前面

為什麼會出現CommonJS規範?

因為JavaScript本身並沒有模組的概念,不支援封閉的作用域和依賴管理,傳統的檔案引入方式又會汙染變數,甚至檔案引入的先後順序都會影響整個專案的執行。同時也沒有一個相對標準的檔案引入規範和包管理系統,這個時候CommonJS規範就出現了。

CommonJS規範的優點有哪些?

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

模組化的定義

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

1.自執行函式

我們先寫一段簡單的程式碼,在node環境下執行,來看看commonJS是如何處理的:

你對CommonJS規範瞭解多少?
一段非常簡單的函式,呼叫時候傳遞引數name,將一段字串返回。但是通過斷點除錯我們發現在node環境下,node本身自動給sayHello函式加了一層外衣,就是下面的內容:

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

我們不難發現,其實這是一個自執行函式,那麼為什麼要加上這樣一段看似多餘的程式碼吶,這就是我們說得CommonJS規範一個好處,它將要執行的函式封裝了起來,所有的變數和方法都可以理解為是私有的了,保證了名稱空間。

2.檔案匯出

前面我們已經瞭解到在node中,每個檔案都可以被看成是一個模組,那麼node中對於模組的匯出,都是使用的相同的方法module.exports。

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

####3.檔案匯入 為了方便的使用模組,我們可以使用require方法對模組進行匯入,類似於這樣:

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

值的注意的是:在檔案引入的過程中,是否使用相對或者絕對路徑,如果a.js前新增./或者../是證明是第三方模組,不寫絕對和相對路徑為內建模組,例如:fs

分析commonJS規範原始碼

我們寫一個簡單的模組引入,通過斷點,分析它的程式碼,並以此為來完善我們自己的commonJS規範

Module._load

你對CommonJS規範瞭解多少?
首先我們能看到第一次進入是require方法中,分析程式碼:

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

Module._resolveFilename

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

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

Module._cache

node中會對已經載入過的模組進行快取,供下次引入時候使用,這個方法就是:Module._cache

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規範瞭解多少?
然後讓這個函式執行

你對CommonJS規範瞭解多少?

手寫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規範的理解和使用,特此說明。

相關文章