美文网首页让前端飞Node.jsJava 核心技术
深入解析node.js的模块加载机制

深入解析node.js的模块加载机制

作者: a333661d6d6e | 来源:发表于2018-11-23 22:06 被阅读2次

    在node.js中,模块使用CommonJS规范,一个文件是一个模块


    • node.js中的模块可分为三类
    • 内部模块 - node.js提供的模块如 fs,http,path等
    • 自定模块 - 我们自己写的模块
    • 第三方模块 - 通过npm安装的模块
      node.js提供了大量的模块供我们使用,比如 想解析一个文件的路径,可以使用path模块下的相应方法实现:
    const path = require('path');
    //返回目标文件的绝对路径
    console.log(path.resolve('./1.txt'));
    

    运行结果:

    /Users/cuiyue/workspace/test/1.txt
    

    使用require引入相应的模块,即可使用。
    __dirname和__filename
    node.js的每个模块都有这两个参数,它们都是一个绝对路径的地址,区别是__filename存放了从根目录到当前文件名的路径,__dirname只存放从根目录到模块的所在目录:

    console.log(__dirname);
    console.log(__filename);
    

    运行结果:

    /Users/cuiyue/workspace/test
    /Users/cuiyue/workspace/test/module.js
    

    vm模块
    vm模块是node.js提供在V8虚拟机中编译和运行的工具,node.js中的模块内部实现就是通过此模块完成。
    说说vm的基本用法。
    在js环境中有一个eval函数,它可以运行js的代码字符串,比如:

    eval('console.log("Hello javascript.")'); //输出Hello javascript.
    

    可以看到,eval函数的参数是一段字符串,它可以运行字符串形式的js代码,但它可以使用上下文环境中的变量:

    var num=100;
    eval('console.log(num)'); //输出100
    

    以上是可以正确访问num的值。
    vm模块提供了方法创建一个安全的沙箱,在指定的上下文环境中运行代码,不受外界干扰。

    const vm = require('vm');
    var num = 100;
    vm.runInThisContext('console.log(num)');
    

    运行结果:

    console.log(num)
                ^
    ReferenceError: num is not defined
    

    可以看到代码报错了,说明在vm创建了指定的上下文环境中,拿不到外界的参量。
    CommonJS规范
    在以前,由于javascript的历史原因导致它的模块机制很差,由于这些缺点使得javascript不太善于开发大型应用,于是提出了CommonJS规范以弥补javascript的不足。
    CommonJS规范主要分为三块内容:模块导入导出、模块定义、模块标识。
    模块导入导出
    CommonJS中使用require()函数进行模块的引入。

    const mymodule = require('mymodule');
    

    使用exports导出模块

    module.exports = {
      name: 'Tom'
    };//欢迎加入全栈开发交流圈一起学习交流:864305860
    

    引用的名称可以不带路径,若不带路径表示引入的是node提供的模块或是npm安装的第三方模块(node_modules)
    模块定义

    • module对象:在每一个模块中,module对象代表该模块自身。
    • export属性:module对象的一个属性,它向外提供接口。

    模块标识

    • 模块标识指的是传递给require方法的参数,必须是符合小驼峰命名的字符串,或者以 .、..、开头的* 相对路径,或者绝对路径。

    node中模块解析流程

    • 首先接收参数,把传入的模块名称解析成绝对路径
    • 若没有后缀名称,依次拼接.js .json .node尝试加载,仍到不到模块则报错
    • 取得正确的路径后判断缓存中是否存在此模块,若有则取出
    • 若缓存中不存在则加载此文件,在外包裹一层闭包并执行它

    以上为大致流程,下面尝试着写一下模块。
    代码的基本结构:

    /**
     * Module类,用于处理模块加载
     */
    function Module() {}
     
    //模块的缓存
    Module._cacheModule = {};
     //欢迎加入全栈开发交流圈一起学习交流:864305860
    //不同扩展名的加载策略
    Module._extensions = {};
     
    //根据moduleId解析绝对路径,
    Module._resolveFileName = function(moduleId) {};
     
    //入口函数
    function req(moduleId) {}
    

    附上全部代码:

    const path = require('path');
    const fs = require('fs');
    const vm = require('vm');
     
    /**
     * Module类,用于处理模块加载
     */
    function Module(file) {
     this.id = file; //当前模块的id,它使用完整的绝对路径标识,因此是唯一的
     this.exports = {}; //导出
     this.loaded = false; //模块是否已加载完毕
    }
     
    //模块的缓存
    Module._cacheModule = {};
     
    Module._wrapper = ['(function(exports,require,module,__dirname,__filename){', '});'];
     
    //不同扩展名的加载策略
    Module._extensions = {
     '.js': function(currentModule) {
      let js = fs.readFileSync(currentModule.id, 'utf8'); //读取出js文件内容
      let fn = Module._wrapper[0] + js + Module._wrapper[1];
      vm.runInThisContext(fn).call(
       currentModule.exports,
       currentModule.exports,
       req,
       currentModule,
       path.dirname(currentModule.id),
       currentModule.id);
      return currentModule.exports;
     },
     '.json': function(currentModule) {
      let json = fs.readFileSync(currentModule.id, 'utf8');
      return JSON.parse(json); //转换为JSON对象返回
     },
     '.node': ''
    };
     
    //加载模块(实例方法)
    Module.prototype.load = function(file) {
     let extname = path.extname(file); //获取后缀名
     return Module._extensions[extname](this);
    };//欢迎加入全栈开发交流圈一起学习交流:864305860
     
    //根据moduleId解析绝对路径,
    Module._resolveFileName = function(moduleId) {
     let p = path.resolve(moduleId);
     
     if (!path.extname(moduleId)) { //传入的模块没有后缀
      let arr = Object.keys(Module._extensions);
     
      //循环读取不同扩展名的文件
      for (var i = 0; i < arr.length; i++) {
       let file = p + arr[i]; //拼接上后缀名成为一个完整的路径
       try {
        fs.accessSync(file);
        return file; //若此文件存在返回它
       } catch (e) {
        console.log(e);
       }
      }
     } else {
      return p;//欢迎加入全栈开发交流圈一起学习交流:864305860
     }//面向1-3年前端人员
    }; //帮助突破技术瓶颈,提升思维能力
    function req(moduleId) {
     let file = Module._resolveFileName(moduleId);
     
     if (Module._cacheModule[file]) { //若缓存中存在此模块
      return Module._cacheModule[file];
     } else {
      let module = new Module(file);
      module.exports = module.load(file);
      return module.exports;
     }
    }//欢迎加入全栈开发交流圈一起学习交流:864305860
    console.log(req('./a.js')());
    

    a.js的文件内容:

    module.exports = function() {
     console.log('This message from a.js');
     console.log(__dirname);
     console.log(__filename);
    }
    

    最终运行结果:

    This message from a.js
    /Users/cuiyue/workspace/test
    /Users/cuiyue/workspace/test/a.js
    

    重要代码说明
    _resolveFileName
    _resolveFileName方法的主要作用是把传入的模块解析成绝对路径,这样才可以进行下一步,根据完整的路径加载模块。
    因此要进行判断,如果传入的模块不存在,则要报错;如果传入的模块已经有扩展名了,就不要拼接了;若没有扩展名,依次以.js .json .node的顺序拼接成完成的模块进行加载。
    _extensions
    此对象中封装了加载不同类型模块的处理方法,其中若是.json类型则使用fs读取文件直接转换成JSON对象并返回。
    若是.js文件则读取后,拼接闭包,将exports,require,module,__dirname,__filename五大参数拼接好,使用vm模块的沙箱机制运行,得到的结果放入module.exports返回。
    总结
    以上就是node.js的模块加载的简单逻辑,实际上node.js的源码远远比上面的代码复杂,光是处理模块路径、判断合法等操作就写了N行。而且我这里没有写缓存以及其它的复杂逻辑,但核心差不多就是这些,核心的核心就是用fs.readFileSync读取js文件,把内容拼接到一个大大的闭包中,这也解释了为什么我们自己写的所有node模块中都会有require方法,exports导出,以及__dirname和__filename参数。
    结语

    感谢您的观看,如有不足之处,欢迎批评指正。

    本次给大家推荐一个免费的学习群,里面概括移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。
    对web开发技术感兴趣的同学,欢迎加入Q群:864305860,不管你是小白还是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时每天更新视频资料。
    最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

    相关文章

      网友评论

        本文标题:深入解析node.js的模块加载机制

        本文链接:https://www.haomeiwen.com/subject/hqzlqqtx.html