實現 CommonJs 規範中的 Require 模組

風辰月發表於2018-06-25

開始之前大家要先熟悉下 node 中常用檔案讀寫,路徑操作等 API

實現思路:我們通過定義一個 req 函式,代替 node 中的 require ,這個函式首先會根據路徑引數進行路徑解析,找到對應檔案;然後判斷快取中是否存在該檔案物件,存在則返回,否則建立該檔案物件,此時不防通過 new 一個 Module 函式例項來建立;再然後,通過 nodefs 模組的 readFileSync 方法載入檔案,並把檔案內容放在閉包函式中,函式引數有exportsrequiremodule,通過 nodevm 模組的 runInThisContext 方法建立一個沙箱環境,同時分別給 exportsrequiremodule 引數賦值並執行,最後快取模組並通過 module.exports 返回檔案內容。

一、構造req函式

詳細程式碼如下:

function req (path) {
  // 先要根據路徑變成一個絕對路徑
  let filename = Module._resolveFilename(path)
  // 檔案路徑唯一
  if (Module._cache[filename]) {
    // 如果載入過 直接把載入過的結果返回
    return Module._cache[filename].exports
  }
  // 通過檔名建立一個模組
  let module = new Module(filename)
  // 載入模組,根據不同字尾載入不同內容
  module.load()
  // 進行模組快取
  Module._cache[filename] = module
  // 返回最後的結果
  return module.exports
}
複製程式碼

二、構造Module函式

詳細程式碼如下:

let path = require('path')
let fs = require('fs')

function Module(filename) {
  // 預設模組未載入過
  this.loaded = false
  // 模組的絕對路徑
  this.filename = filename
  // 模組匯出的結果
  this.exports = {}
}
複製程式碼

路徑解析方法如下:

Module._resolveFilename = function (p) {
  p = path.join(__dirname, p)
  // 如果檔案有字尾
  if (!/\.\w+$/.test(p)) {
    // 新增副檔名
    for (let i = 0; i < Module._extensions.length; i++) {
      // 拼出一個路徑
      let filePath = p + Module._extensions[i]
      try {
        // 判斷檔案是否存在
        fs.accessSync(filePath)
        // 返回檔案路徑
        return filePath
      } catch (e) {
        // 如果accessSync方法報錯,說明檔案不存在,則丟擲檔案未找到異常
        if (i > Module._extensions.length) {
          throw new Error('module not found')
        }
      }
    }
  } else {
    // 否則直接返回檔案路徑
    return p
  }
}
複製程式碼

Module 函式的主要功能即是 load 方法,如果目標檔案是 js 檔案,則按照 js 方式載入,如果目標檔案是 json 檔案,則按照 json 方式載入。詳細程式碼如下:

let vm = require('vm')
// 模組載入函式
Module.prototype.load = function () {
  // 取到檔名稱字尾
  let extname = path.extname(this.filename)
  // 根據不同字尾讀取檔案內容
  Module._extensions[extname](this)
  // 標記模組已載入
  this.loaded = true
}

// 模組快取物件
Module._cache = {}

// 檔案字尾
Module._extensions = ['.js', '.json']

// json格式檔案讀取方式
Module._extensions['.json'] = function (module) {
  let content = fs.readFileSync(module.filename, 'utf8')
  module.exports = JSON.parse(content)
}

// 建立執行js沙箱環境時需要
Module.wrapper = ['(function (exports, require, module){', '\n})']

// js程式碼拼接
Module.wrap = function (content) {
  return Module.wrapper[0] + content + Module.wrapper[1]
}

// js格式檔案讀取方式
Module._extensions['.js'] = function (module) {
  // 讀取檔案
  let content = fs.readFileSync(module.filename, 'utf8')
  // 把檔案內容放到閉包函式中
  let script = Module.wrap(content)
  // 建立不影響外界上下文的沙箱環境
  let fn = vm.runInThisContext(script)
  // 讓閉包函式執行,同時給函式中export require module變數賦值
  fn.call(module.exports, module.exports, req, module)
}
複製程式碼

好了,實現起來是不是很簡單,下面測試一下:

// 在同級目錄下建立test.js
// module.exports = 'Hello World'
let str = req('./test')
console.log(str)
複製程式碼

原始碼

好了,到這裡全部程式碼已經給出,就到此為止吧~ ???

相關文章