基於CommonJS規範,簡單實現NodeJs模組化

香海流洋發表於2018-09-13

目錄

CommonJS

image

CommonJS是一個模組化的規範,Nodejs的模組系統,就是參照CommonJS規範實現的。每個檔案就是一個模組 ,每個模組都要自己的作用域。

特點

  1. 所有程式碼都有執行在模組作用域,不會汙染全域性作用域
  2. 模組可以多次載入,但是隻會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。要想讓模組再次執行,必須清除快取。
  3. 模組載入的順序,按照其在程式碼中出現的順序。

NodeJs 模組化簡單實現

首先我們先了解一下模組化載入:

  1. 模組載入
  2. 模組解析檔名,解析出絕對路徑來
  3. 我們引入時可以省略檔案的字尾,有可能沒有寫字尾名, 會自動查詢對應目錄下 .js.json的檔案
  4. 多次呼叫只走一次, 得到一個真實存在的檔案路徑,在快取中看檔案是否存在,不存在則建立模組,然後放入到快取中,方便別人讀取
  5. 讀取檔案內容。如果是 .js 檔案,就把內容加一個閉包
  6. 執行JS指令碼,將結果返回

0. 建立module建構函式

每個模組內部,都有一個 module 物件,代表當前模組。

檢視 console.log(module) module都有哪些屬性:

  • module.id 模組的識別符,通常是帶有絕對路徑的模組檔名
  • module.filename 模組的檔名,帶有絕對路徑
  • module.loaded 返回一個布林值,表示模組是否載入
  • module.parent 返回一個物件,表示呼叫該模組的模組
  • module.children 返回一個陣列,表示該模組要用到的其他模組
  • module.exports 表示模組對外輸出的值,預設值為空物件
function Module(id) {
    // 下面就簡單定義兩個常用的
    this.id = id
    this.exports = {}
}
複製程式碼

1. 模組載入

引入自己寫的模組
// 我們自己寫的檔案模組 要寫路徑 ./ 或者../
let hello = require('./1.NodeJS')
console.log(hello) // first NodeJS
複製程式碼
引入內建模組就直接寫名稱即可
//操作檔案的模組
let fs = require('fs')

// 處理路徑的模組
let path = require('path')

// 虛擬機器模組,沙箱執行,防止變數汙染
let vm = require('vm')
複製程式碼

2. 解析檔案,返回絕對路徑

Module._resolveFilename 方法實現的功能是判斷使用者引入的模組是否包含字尾名,如果沒有字尾名會根據 Module._extensions 的鍵的順序查詢檔案,直到找到字尾名對應的檔案的絕對路徑,優先查詢 .js

// 將引入檔案處理為絕對路徑
Module._resolveFilename = function (p) {
  // 以js或者json結尾的
  if ((/\.js$|\.json$/).test(p)) {
    // __dirname當前檔案所在的資料夾的絕對路徑
    // path.resolve方法就是幫我們解析出一個絕對路徑出來
    return path.resolve(__dirname, p);
  } else {
    // 沒有後字尾  自動拼字尾 
    // Module._extensions 處理不同字尾的模組
    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方法對路徑進行查詢,找不到對應檔案直接報錯 
        fs.accessSync(temp)
        realPath = temp
        break
      } catch (e) {
      }
    }
    if (!realPath) {
      throw new Error('module not exists');
    }
    // 將存在絕對路徑返回
    return realPath
  }
}
複製程式碼

Module._extensions 處理對應模組副檔名。這裡我們只提 .js .json,對應模組的處理功能我們在後面來實現

Module._extensions = {
    "js": function() {},
    "json": function() {}
}
複製程式碼

3. 多次呼叫只一次

當使用者重複載入一個已經載入過的模組,我們只有第一次是載入,然後放入快取中,後面在載入時,直接返回快取中即可

Module._cacheModule = { } 存放模組快取

// 獲取記憶體中的結果
let cache = Module_cacheModule[filename]
// 判斷是否存在
if (cache) {
    // 如果存在直接將 exports物件返回
    return cache.exports
}
// 如果不存在記憶體中,就建立模組,然後加入到記憶體中
let module = new Module(filename)
Module._cacheModule[filename] = module
複製程式碼

4. 載入模組,對於不同型別的檔案做不同的處理

根據前面我們說到的模組化,對於已經存放在記憶體中的我們直接返回就可以了,對於新新增的模組,我們該讀取檔案了, 根據傳入的模組,嘗試載入模組方法

// 根據傳入的模組,嘗試載入模組方法
function tryModuleLoad(module) {
  // 前面我們已經提到 module.id 為模組的識別符,通常是帶有絕對路徑的模組檔名
  // path.extname 獲取檔案的副檔名
  /* let ext = path.extname(module.id);
  // 如果副檔名是js 呼叫js處理器 如果是json 呼叫json處理器
  Module._extensions[ext](module); // exports 上就有了陣列 */
  let ext = path.extname(module.id);//副檔名
  // 如果副檔名是js 呼叫js處理器 如果是json 呼叫json處理器
  Module._extensions[ext](module); // exports 上就有了陣列
}
複製程式碼

Module._extensions 處理對應字尾名模組。這裡我們只提 .js .json

// 處理對應字尾名模組
Module._extensions = {
  ".js": function (module) {
    // 對於js檔案,讀取內容
    let content = fs.readFileSync(module.id, 'utf8')
    // 給內容新增閉包, 後面實現
    let funcStr = Module.wrap(content)
    // vm沙箱執行, node內建模組,前面我們已經引入, 將我們js函式執行,將this指向 module.exports
    vm.runInThisContext(funcStr).call(module.exports, module.exports, req, module)
  },
  ".json": function (module) {
    // 對於json檔案的處理就相對簡單了,將讀取出來的字串轉換未JSON物件就可以了
    module.exports = JSON.parse(fs.readFileSync(module.id, 'utf8'))
  }
}
複製程式碼

上面我們使用了 Module.wrap 方法,是幫助我們新增一個閉包,簡單說就是我們在外面包了一個函式的前半段和後半段

// 存放閉包字串
Module.wrapper = [
  "(function (exports, require, module, __filename, __dirname) {",
  "})"
]
複製程式碼
// 將我們讀到js的內容傳入,組合成閉包字串
Module.wrap = function (script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
}
複製程式碼

5. 執行並返回結果

要執行,我們來看一下完整的模組載入程式碼

// 模組載入
Module._load = function (f) {
  // 相對路徑,可能這個檔案沒有字尾,嘗試加字尾
  let fileName = Module._resolveFilename(f); // 獲取到絕對路徑
  // 判斷快取中是否有該模組
  if (Module._cacheModule[fileName]) {
    return Module._cacheModule[fileName].exports
  }
  let module = new Module(fileName); // 沒有就建立模組
  Module._cacheModule[fileName] = module // 並將建立的模組新增到快取

  // 載入模組
  tryModuleLoad(module)
  return module.exports
}
複製程式碼

到這裡,一個簡單的module就實現了,讓我們測試一下吧

// 測試程式碼
function req(p) {
  return Module._load(p); // 載入模組
}

// 第一次沒有快取,建立module,並新增到快取中
let str = req('./1.NodeJS');
// 第二次就是返回的快取中的
let str1 = req('./1.NodeJS.js');
console.log(str) // first NodeJS
console.log(str1) // first NodeJS
複製程式碼

附原始碼

重要為了方便大家瞭解、檢視、除錯程式碼,完整的原始碼參見gitHub

總結

本篇文章是基於CommonJS規範,實現了一個簡單的NodeJS模組化,主要目的在於理解 NodeJS 模組化的實現思路,希望對大家瞭解模組化起到一定的作用。

作者:香香

將來的你,一定會感謝現在拼命努力的自己!

Node基礎系列文章

相關文章