likes
comments
collection
share

面试官:试着说一下commonjs模块化加载过程,并试着手写一个简易版的commonjs

作者站长头像
站长
· 阅读数 17

我们知道commonjs适用于nodejs中,因为他是同步加载文件模块的,由于文件模块都在本地磁盘,加载速度比较快,所以可以在nodejs中盛行。

nodejs中的内置模块在nodejs运行的时候就已经被作为二进制加载到内存中,require时直接在内存中取出。

加载流程分析

  • 路径分析:确定目标模块位置 (内置,第三方(module.paths),本地)

  • 文件定位:确定目标中的具体文件。(js/json/node -> package.json中的main -> index.js/json/node)

  • 编译执行:对模块内容进行编译,返回可用的exports对象。(js -> 函数调用,传递参数。json -> JSON.parse())

源码解析

  • relResolveCacheIdntifier 根据路径查找该路径是否被缓存。
  • 无,解析并补全路径为完整的路径
  • 根据路径查找模块是否被缓存。
  • 是否是内置模块
  • 创建一个module对象,并缓存该模块。 面试官:试着说一下commonjs模块化加载过程,并试着手写一个简易版的commonjs
  • module.load() 初始化一些模块属性。
  • 通过后缀取出对应的函数。 面试官:试着说一下commonjs模块化加载过程,并试着手写一个简易版的commonjs
  • 函数内通过readFileSync同步加载文件内容。 面试官:试着说一下commonjs模块化加载过程,并试着手写一个简易版的commonjs
  • module._compile() 内部将文件内容包裹在一个全局函数中。即每个模块的作用域函数。
  • 通过获取require、exports、module、__dirname、__filename传入并调用全局函数,返回结果。其中this被绑定为空对象。 面试官:试着说一下commonjs模块化加载过程,并试着手写一个简易版的commonjs

自己实现commonjs模块化

  • 通过fs.readFileSync读取文件,eval执行字符串。

缺点:没有自己的作用域。

    const fs = require("fs")

    const content = fs.readFileSync("./demo.js", "utf-8") // var  a = 10

    eval(content)

    console.log("a", a) // 10
  • 通过Function执行字符串。

缺点:定义麻烦。


const fs = require("fs")

const content = fs.readFileSync("./demo.js", "utf-8") // var  a = 10

const fn = new Function("", `${content} return a`);

console.log(fn()) // 10
  • 通过nodejs内置的核心模块VM

可以创建一个独立运行的沙箱环境。


const fs = require("fs")
const vm = require("vm")

const b = 100
a = 100 // 可以被使用
const content = fs.readFileSync("./demo.js", "utf-8") // var  a = 10


// vm.runInThisContext(content) // 内部是一个沙箱,不能使用外部变量。但是外部可以使用沙箱内的内容
// console.log(a) // 10

vm.runInThisContext("a = a + 10") // 但是沙箱内可以使用外部为使用声明定义的变量
console.log(a) // 110

实现

const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')

function Module (id) {
  // 存储文件路径
  this.id = id
  // 存储导出内容
  this.exports = {}
}

// 分析传入的路径
Module._resolveFilename = function (filename) {
  // 利用 Path 将 filename 转为绝对路径
  let absPath = path.resolve(__dirname, filename)
  
  // 判断当前路径对应的内容是否存在()
  if (fs.existsSync(absPath)) { // 这里查到文件夹也是可以通过的
    // 如果条件成立则说明 absPath 对应的内容是存在的
    // 如果是文件夹取index.js,否则抛出错误
    if(fs.statSync(absPath).isDirectory()) {
      if(fs.existsSync(path.join(absPath, "index.js"))) {
        return path.join(absPath, "index.js")
      }else {
        throw new Error(`${filename} is not exists`)
      }
    }else {
      return absPath
    }
  } else {
    // 文件定位
    let suffix = Object.keys(Module._extensions)

    for(var i=0; i<suffix.length; i++) {
      let newPath = absPath + suffix[i]
      if (fs.existsSync(newPath)) {
        return newPath
      }
    }
  }
  throw new Error(`${filename} is not exists`)
}

// 定义查询的扩展名和函数映射
Module._extensions = {
  '.js'(module) {
    // 读取
    let content = fs.readFileSync(module.id, 'utf-8')

    // 包装
    content = Module.wrapper[0] + content + Module.wrapper[1] 
    
    // VM 
    let compileFn = vm.runInThisContext(content)

    // 准备参数的值
    let exports = module.exports
    let dirname = path.dirname(module.id)
    let filename = module.id

    // 调用
    compileFn.call(exports, exports, myRequire, module, filename, dirname)
  },
  '.json'(module) {
    let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))

    module.exports = content
  }
}

Module.wrapper = [
  "(function (exports, require, module, __filename, __dirname) {",
  "})"
]

// 缓存对象 (绝对路径:module对象)
Module._cache = {}

// 加载模块,取出扩展名执行对应函数
Module.prototype.load = function () {
  let extname = path.extname(this.id)
  
  Module._extensions[extname](this)
}

function myRequire (filename) {
  // 1 绝对路径
  let mPath = Module._resolveFilename(filename)
  
  // 2 缓存优先
  let cacheModule = Module._cache[mPath]
  if (cacheModule) return cacheModule.exports

  // 3 创建空对象加载目标模块
  let module = new Module(mPath)

  // 4 缓存已加载过的模块
  Module._cache[mPath] = module

  // 5 执行加载(编译执行)
  module.load()

  // 6 返回数据
  return module.exports
}