• 学习笔记—Node中require的实现


    日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。


    require

    在上一篇文章中,我们了解到了如何去通过 调试查看Node源码

    // 使用 require 引入文件
    // a.js
    var a = 100;
    module.exports = a;
    // b.js
    let a = require('./a')
    console.log(a)
    

    通过调试查看 require 方法源码,其实现思路主要为以下几点。

    1. require 方法的是 Module 模块的原型方法,也就是 Module.prototype.require
    2. 通过 Module._resolveFilename 方法,将传入的路径转换为绝对路径。并添加文件的后缀名。(.js、.json 等)
    3. new Module 拿到转换完毕的绝对路径,并创造一个模块并导出。(其中包含一个属性id [ 当前文件路径 ],还有一个 exports
    4. Module.load 对模块进行加载。
    5. 根据文件后缀 Module._extensions['.js'] 去做策略加载。
    6. fs.readFileSync 同步读取文件。
    7. 增加了一个函数的外壳 ( wrapper包装 ) 让这个函数执行,并且让 Module.exports 作为当前上下文的 this
    8. 最终用户会拿到 Module.exports 的封装后的返回结果。

    所以,最终会返回一个 Module.exports 对象。通过以上思路,我们就可以实现一套 require 方法。

    实现require方法

    根据上述规则,我们可以模拟实现一套 require 方法。

    const fs = require('fs');
    const path = require('path');
    const vm = require('vm');
    
    function Module(id) {
        this.id = id;
        this.exports = {}; 
    }
    Module.wrapper = [
        `(function(exports,require,module,__filename,__dirname){`,
        `})`
    ];
    Module._extensions = {
        '.js'(module) {
            let content = fs.readFileSync(module.id, 'utf8');
            content = Module.wrapper[0] + content + Module.wrapper[1];
            let fn = vm.runInThisContext(content);
            let exports = module.exports;
            let dirname = path.dirname(module.id);
            fn.call(exports, exports, _require, module, module.id, dirname);
        },
        '.json'(module) {
            let content = fs.readFileSync(module.id, 'utf8');
            module.exports = JSON.parse(content);
        }
    }
    Module._resolveFilename = function (filename) {
        let filePath = path.resolve(__dirname, filename);
        let isExists = fs.existsSync(filePath);
        if (isExists) {
            return absPath;
        } else {
            let keys = Object.keys(Module._extensions);
    	for (let i = 0; i < keys.length; i++) {
    		let newPath = filePath + keys;
    		if (fs.existsSync(newPath)) return newPath;
    	}
    	throw new Error('module not exists')
        }
    }
    Module.prototype.load = function () {
        let extName = path.extname(this.id);
        Module._extensions[extName](this);
    }
    Module._cache = {};
    
    function _require(filename) {
        filename = Module._resolveFilename(filename);
        let cacheModule = Module._cache[filename];
        if (cacheModule) {
            return cacheModule.exports; 
        }
        let module = new Module(filename);
        Module._cache[filename] = module
        module.load();
        return module.exports;
    }
    

    现在我们来进行一步一步的解析。

    1. 首先,我们需要先引入需要用到的内置模块(fspathvm)。

      自定义一个 函数方法 _require ,这就是我们最终需要实现的方法,第一个参数接受传入的路径。

      const fs = require('fs');
      const path = require('path');
      const vm = require('vm');
      function _require(filename) {
      	// ...
      }
      
    2. 随后我们还需要一个 Module._resolveFilename 方法将传入的路径转换成绝对路径,并添加后缀。

      因为使用 Module 方法,我们我们也需要声明一个名为 Module 的构造函数。

      随后将路径传入 Module._resolveFilename 方法中。

      function Module() {}
      Module._resolveFilename = function (id) {
          let filePath = path.resolve(__dirname, id);
          console.log(filePath); // d:\xxx\xxx\xxx\a
      }
      function _require(filename) {
          filename = Module._resolveFilename(filename); // 绝对路径
      }
      
    3. 我们发现目前打印的结果是没有后缀的(不确定用户是否填写后缀),所以我们需要使用 fs.existsSync 判断当前路径是否存在。

      Module._resolveFilename = function (id) {
        // ...
        let isExists = fs.existsSync(filePath);
        if (isExists) return filePath;
      }
      

      如果存在,则直接返回结果。如果不存在,我们就需要给当前路径尝试添加后缀。

    4. 这里我们就需要添加后缀,我们需要先定义一个 Module._extensions 方法来对后缀进行分类。

      Module._extensions = {
          '.js'() {},
          '.json'() {}
      }
      

      然后我们将定义的后缀方法的 keys 添加到路径上,并再次进行路径判断。

      路径存在则直接返回结果,如果路径不存在,这次就需要 返回一个错误

      Module._resolveFilename = function (id) {
      	if(isExists){
            // ...
      	} else {
            let keys = Object.keys(Module._extensions);
            for (let i = 0; i < keys.length; i++) {
                let newPath = filePath + keys;
                if (fs.existsSync(newPath)) return newPath;
            }
            throw new Error('module not exists')
        }
      }	
      

      这样就可以保证我们 传入的路径,无论是加后缀或者不加后缀,都会返回一个 当前路径的绝对路径,且 一定会找到当前文件

    5. 我们就已经创建好了一个 绝对引用路径,方便我们后续进行读取。

      现在我们就需要根据这个路径,创建一个可以导出的模块。

      这个模块就属于 Module 构造函数,根据我们一开始查看源码时总结的定义,我们知道 模块全部都是通过 Module.exports 方法进行导出的。

      function Module(id) {
          this.id = id; // 绝对路径
          this.exports = {}; // 默认导出的是空对象
      }
      
      function _require(filename) {
          filename = Module._resolveFilename(filename); // 绝对路径
          let module = new Module(filename);
          return module.exports;
      }
      

      这样我们路径和导出的架子就有了,现在我们需要对中间部分进行处理。

    6. 其实所谓的 中间部分,也就是让用户对 Module.exports 赋值(目前导出的是空对象)

      根据源码的定义,我们需要定义一个 module.load 来对模块进行加载。

      Module.prototype.load = function () {
          let extName = path.extname(this.id); // 获取后缀名
          Module._extensions[extName](this);
      }
      function _require(filename) {
          // ...
          module.load();
      }
      

      这种定义的好处就是,我们可以根据传入的后缀名,调用不同的处理策略。实现文件的 策略加载

      这样我们的 module 就会被传到上面的 Module._extensions 方法中

      Module._extensions = {
          '.js'(module) {},
          '.json'(module) {}
      }
      

      下一步我们就需要完善一下 Module._extensions 方法。

    7. 我们先来完善一下 json 方法,因为这个是最好实现的。

      我们先随便定义一个 .json 文件来进行测试。

      // a.json
      {
      	"name" : "MXShang",
      	"age" : 26
      }
      

      然后我们来完善一下 Module._extensions[json] 方法

      Module._extensions = {
          '.js'() {},
          '.json'(module) {
              let content = fs.readFileSync(module.id, 'utf8')
              module.exports = JSON.parse(content);
          }
      }
      

      获取绝对路径,通过 fs.readFileSync 同步读取内容,并输出。

      很好理解也很简单,接下来我们看一下 Module._extensions[js]

    8. 在实现 Module._extensions[js] 方法前,我们先需要完成一个函数的外壳 ( wrapper包装 ) ,也就是我们之前文章中经常提到的,包含五个参数的函数。

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

      然后我们来实现 Module._extensions[js] 方法。

      思路与实现 .json 相似,先将绝对路径的内容读出来,并将内容放到 wrapper 中。。

      Module._extensions = {
          '.js'(module) {
              let content = fs.readFileSync(module.id, 'utf8');
              content = Module.wrapper[0] + content + Module.wrapper[1];
          }
      }
      

      这样我们就可以得到一个 被wrapper包裹的代码字符串

    9. 现在我们来将字符串变成可以执行的函数。使用 vm.runInThisContext 将字符串变成函数。

      Module._extensions = {
          '.js'(module) {
              // ... 
            	let fn = vm.runInThisContext(content); // 获取最终执行的函数
          }
      }
      

      现在我们需要明确一下 fn 的执行位置,也就是其 this的指向

      不用多说,this一定是指向 module.exports 的,所以我们需要通过 fn.call 方法来将函数的this指向当前构造函数。

      然后我们再根据最终的函数,依次获取一下需要传递的五个参数。

      Module._extensions = {
          '.js'(module) {
              // ... 
            	let exports = module.exports; // 当前this也就是exports参数。this = exports = module.exports
              let dirname = path.dirname(module.id); // 当前文件执行位置的绝对略经
            	fn.call(exports, exports, _require, module, module.id, dirname); 
          }
      }
      

      到了这一步我们就可以发现,require 方法实际上就是通过 Module 作为一个 中间层 来实现的。

      至此,我们的 require 方法的整体思路就实现了。

      但是我们还有一个小问题,就是如果我们多次引入文件,是没有缓存的。所以我们需要 对结果进行缓存

    10. 定义一个 Module._cache 来对结果进行缓存。

      // 定义一个 Module._cache
      Module._cache = {};
      
      // 在模块加载前,先将定义好的module模块结果进行缓存
      function _require(filename) {
          // ...
          Module._cache[filename] = module; // 根据文件名进行缓存
          module.load();
          return module.exports;
      }
      

      如果当前模块已经被缓存过 (加载过) ,直接将缓存好的模块导出就可以了。

      function _require(filename) {
          // ...
          let cacheModule = Module._cache[filename];
          if (cacheModule) {
              return cacheModule.exports; 
          }
          let module = new Module(filename);
          Module._cache[filename] = module; // 根据文件名进行缓存
          module.load();
          return module.exports;
      }
      

      这样我们就实现了一套 require 方法。

    通过阅读源码,并通过源码手写方法,可以使我们更好的使用方法,也可以提升我们的技术。

    本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
    您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
    最后感谢您的支持!

  • 相关阅读:
    微软面试100 题题解
    二元查找树转变成排序的双向链表(树)
    筆試
    PE注入
    内核网络通信
    哈哈哈
    OpenCV 学习
    第一次研究VM程序中的爆破点笔记
    SE2.3.4各试用限制调试笔记
    破解相关书籍
  • 原文地址:https://www.cnblogs.com/moxiaoshang/p/15585427.html
Copyright © 2020-2023  润新知