• 【 js 模块加载 】【源码学习】深入学习模块化加载(node.js 模块源码)


    文章提纲:

      第一部分:介绍模块规范及之间区别

      第二部分:以 node.js 实现模块化规范 源码,深入学习。

    一、模块规范

    说到模块化加载,就不得先说一说模块规范。模块规范是用来约束每个模块,让其必须按照一定的格式编写。AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范。 

    1、AMD(Asynchronous Module Definition):异步模块定义,所谓异步是指模块和模块的依赖可以被异步加载,他们的加载不会影响它后面语句的运行。有效避免了采用同步加载方式中导致的页面假死现象。AMD代表:RequireJS。
     
    它主要有两个接口:define 和 require。define 是模块开发者关注的方法,而 require 则是模块使用者关注的方法。 
          1.1、define() 函数:

    define(id?, dependencies?, factory);
    //id :可选参数,它指的是模块的名字。
    //dependencies:可选参数,定义中模块所依赖模块的数组。
    //factory:模块初始化要执行的函数或对象
    需要注意的是,dependencies有多少个元素,factory就有多少个传参,位置一一对应。
    使用栗子:
    1 define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {  
    2     exports.verb = function() {            
    3         return beta.verb();            
    4         //Or:
    5         //return require("beta").verb();        
    6     }    
    7 });

          1.2、require() 函数

    require([module], callback);
    //module:一个数组,里面的成员就是要加载的模块.
    //callback:模块加载成功之后的回调函数。

    需要注意的是 ,module 有多少个元素,callback 就有多少个传参,位置一一对应。

    使用的栗子:

    require(["a","b","c"],function(a,b,c){
        //code here
    });

    具体的使用详细,大家可以去官网学习:https://github.com/amdjs/amdjs-api/wiki/AMD-(%E4%B8%AD%E6%96%87%E7%89%88)
     
    2、CMD(Common Module Definition):通用模块定义,本质上也是异步的加载模块,采用的是懒加载方式即按需加载。CMD代表:SeaJS。
     
    它主要有一个接口:define 是模块开发者关注的方法。
    define(factory);
    //factory:模块初始化要执行的函数或对象,为函数时,表示是模块的构造方法。
    //执行该构造方法,可以得到模块向外提供的接口。
    //factory 方法在执行时,默认会传入三个参数:require、exports 和 module。
    //其中require用来获取其他模块提供的接口,exports用来向外提供模块接口,module是一个对象,上面存储了与当前模块相关联的一些属性和方法。

    使用的栗子: 

     1 define(function(require, exports, module) {
     2     var a = require('./a')
     3     a.doSomething()
     4     // 此处略去 100 行
     5     var b = require('./b') // 依赖可以就近书写
     6     b.doSomething()
     7     //
     8     // 对外提供 doSomething 方法
     9     exports.doSomething = function() {};
    10 });

    而调用CMD编写的模块的方法是:

    1 seajs.use("a")//调用a模块
    2 //这里就设计到SeaJS的使用了:
    3 //- 引入sea.js的库
    4 //- 如何变成模块?
    5 //      - define
    6 //- 如何调用模块?
    7 //      -sea.js.use
    8 //- 如何依赖模块?
    9 //      -require

    具体的使用详细,建议大家可以去官网学习:https://github.com/seajs/seajs/issues/242
     
     
    3、CommonJS :采用同步加载模块的方式,也就是说只有加载完成,才能执行后面的操作。CommonJS 代表:Node 应用中的模块,通俗的说就是你用 npm 安装的模块。
    它使用 require 引用和加载模块,exports 定义和导出模块,module 标识模块。使用 require 时需要去读取并执行该文件,然后返回 exports 导出的内容。
     1  //定义模块 math.js
     2  var random=Math.random()*10;
     3  function printRandom(){
     4      console.log(random)
     5  }
     6 
     7  function printIntRandom(){
     8      console.log(Math.floor(random))
     9  }
    10  //模块输出
    11  module.exports={
    12      printRandom:printRandom,
    13      printIntRandom:printIntRandom
    14  }
    15  //加载模块 math.js
    16  var math=require('math')
    17  //调用模块提供的方法
    18  math.printIntRandom()
    19  math.printRandom()

    4、模块规范之间的区别

         A、首先说一下 CommonJS与其它两种的区别:CommonJS采用的就是同步加载方式,而其它两种都是异步的。

       举个栗子:

    commonJS中:

    1 var math = require('math');
    2 math.add(2, 3);

    第二行 math.add(2, 3),在第一行 require('math') 之后运行,因此必须等 math.js 加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

    AMD中:

    1 require(['math'], function (math) {
    2   math.add(2, 3);
    3 });
    4 
    5 console.log("222");

    这个是不会阻遏后面语句的执行的,等到什么时候 math 模块加载出来进行回调函数就可以了。

    PS:由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。 
     
         B、再说一下 AMD 和 CMD 的区别:
    最主要的区别就是执行模块的机制大不一样:
    SeaJS(CMD) 对模块的态度是懒执行, 而 RequireJS(AMD) 对模块的态度是预执行。
    SeaJS 只会在真正需要使用(依赖)模块时才执行该模块。执行模块的顺序也是严格按照模块在代码中出现(require)的顺序。
    而 RequireJS 会先尽早地执行(依赖)模块, 相当于所有的 require 都被提前了, 而且模块执行的顺序也不一定100%按照顺序。 
      
    如果大家没有明白,可以参考文章:https://www.douban.com/note/283566440/  有栗子,更形象。 
     
    二、深入学习模块化加载
     
    下面从源码深入了解一下所谓的模块加载系统到底是如何运作的。
    因为现在工作中用的大部分模块都是 node_modules 也就是 CommonJS 模块规范,所以我就以 node.js 实现模块的源码来分析:
     
    前提知识:
          知识点一:主入口文件 即主模块。在 require 方法中引用的 Module._load(path,parent,isMain),第三个参数 isMain 表示是不是主入口文件。对于 foo.js 文件,如果通过 node foo.js 运行则为 true,但如果通过 require('./foo') 运行则为 false。
         知识点二:涉及到的模块类型:
         1、核心模块:指的 lib 目录下排除 lib/internal 文件下的模块。是那些被编译进 Node 的二进制模块,它们被预置在 Node 中,提供 Node 的基本功能,如fs、http、https等。核心模块使用 C/C++ 实现,外部使用 JS 封装。要加载核心模块,直接在代码文件中使用 require() 方法即可,参数为模块名称,Node 将自动从核心模块文件夹中进行加载。注意加载核心模块只能用模块名。核心模块拥有最高的加载优先级,即使已经有了一个同名的第三方模块,核心模块也会被优先加载。
         2、内部模块:指的是 lib/internal 文件夹下的模块,这些模块仅仅供 Node.js 核心的内部使用,不能被外部使用。
     

      

    通常我们在使用一个模块的时候在 js 中都是这样引用的:

    var math = require('math');
    math.add(2, 3);

    从 require 方法本身是如何实现的入手,一步一步看:(代码全部来自 node.js [https://github.com/nodejs/node] 源码)

    require 方法封装在 node 源码中的 lib 文件夹里的 module.js 中

     1 // Loads a module at the given file path. Returns that module's
     2 // `exports` property.
     3 // 给定一个模块目录,返回该模块的 exports 属性
     4 Module.prototype.require = function(path) {
     5   // assert() 头部引入,主要用于断言,如果表达式不符合预期,就抛出一个错误。
     6   // assert方法接受两个参数,当第一个参数对应的布尔值为true时,不会有任何提示,返回undefined。
     7   // 当第一个参数对应的布尔值为false时,会抛出一个错误,该错误的提示信息就是第二个参数设定的字符串。
     8   assert(path, 'missing path');  //断言是否有path
     9   assert(typeof path === 'string', 'path must be a string'); //断言 path是否是个字符串
    10 
    11   return Module._load(path, this, /* isMain */ false);  //require方法主要是为了引出_load方法。
    12   //_load函数三个参数: path 当前加载的模块名称,parent 父亲模块,其实是谁导入了该模块,
    13   // /* isMain */ false  是不是主入口文件
    14 };

    require 中调用了 Module._load()方法:
     1 // Check the cache for the requested file.
     2 // 1. If a module already exists in the cache: return its exports object.
     3 // 2. If the module is native: call `NativeModule.require()` with the
     4 //    filename and return the result.
     5 // 3. Otherwise, create a new module for the file and save it to the cache.
     6 //    Then have it load  the file contents before returning its exports
     7 //    object.
     8 // 从缓存中查找所要加载的模块
     9 // 1. 如果一个模块已经存在于缓存中:直接返回它的exports对象
    10 // 2. 如果模块是一个本地模块,调用'NativeModule.require()'方法,filename作为参数,并返回结果
    11 // 3. 否则,使用这个文件创建一个新模块并把它加入缓存中。在加载它只会返回exports对象。
    12 // _load函数三个参数: path 当前加载的模块名称,parent 父亲模块,/* isMain */ false  是不是主入口文件
    13 Module._load = function(request, parent, isMain) { 
    14   if (parent) {
    15       //头部引入了 Module._debug = util.debuglog('module');const debug = Module._debug;
    16     // 这个方法用来打印出调试信息,具体可以看 https://chyingp.gitbooks.io/nodejs/%E6%A8%A1%E5%9D%97/util.html
    17     debug('Module._load REQUEST %s parent: %s', request, parent.id); 
    18 
    19   }
    20 
    21   // 找到当前的需要解析的文件名
    22   var filename = Module._resolveFilename(request, parent, isMain);
    23 
    24   //如果已经有的缓存,直接返回缓存的exports
    25   var cachedModule = Module._cache[filename];
    26   if (cachedModule) {
    27     return cachedModule.exports;
    28   }
    29 
    30   //如果模块是一个内部模块,调用内部方法'NativeModule.require()'方法,filename作为参数,并返回结果
    31   if (NativeModule.nonInternalExists(filename)) {
    32     debug('load native module %s', request);
    33     return NativeModule.require(filename);
    34   }
    35 
    36   //创建一个新模块
    37   var module = new Module(filename, parent);
    38 
    39   //是否为主模块,
    40   if (isMain) {
    41     //主模块的话,需要将当前的module赋值给process.mainModule
    42     process.mainModule = module;
    43     //主模块的id特殊的赋值为"."
    44     module.id = '.';
    45   }
    46 
    47   //并把新模块加入缓存中
    48   Module._cache[filename] = module;
    49 
    50   //尝试导入模块的操作
    51   tryModuleLoad(module, filename);
    52 
    53   // 返回新创建模块中的exports,也就是暴露在外面的方法属性等。
    54   return module.exports;
    55 };

    Module._load 中调用了 Module._resolveFilename() 方法

     1 // 负责具体filename的文件查找
     2 // 参数 request 当前加载的模块名称,parent 父亲模块,/* isMain */ false  是不是主入口文件
     3 Module._resolveFilename = function(request, parent, isMain) { 
     4 
     5   //NativeModule用于管理js模块,头部引入的。
     6   //NativeModule.nonInternalExists()用来判断是否是原生模块且不是内部模块,
     7   //所谓内部模块就是指 lib/internal 文件目录下的模块,像fs等。
     8   //满足 是原生模块且不是内部模块,则直接返回 当前加载的模块名称request。
     9   if (NativeModule.nonInternalExists(request)) {
    10     return request;
    11   }
    12 
    13   // Module._resolveLookupPaths()函数返回一个数组[id , paths], 
    14   // paths是一个 可能 包含这个模块的文件夹路径(绝对路径)数组
    15   var paths = Module._resolveLookupPaths(request, parent, true);
    16 
    17   // look up the filename first, since that's the cache key.
    18   // 确定哪一个路径为真,并且添加到缓存中
    19   var filename = Module._findPath(request, paths, isMain);
    20 
    21   // 如果没有找到模块,报错
    22   if (!filename) {
    23     var err = new Error(`Cannot find module '${request}'`);
    24     err.code = 'MODULE_NOT_FOUND';
    25     throw err;
    26   }
    27 
    28   // 找到模块则直接返回
    29   return filename;
    30 };

    Module._resolveFilename 调用了 Module._resolveLookupPaths() 方法 和 Module._findPath() 方法。

    这两个方法主要是对模块路径的查找,这里要说一下 node 模块路径解析,方便对下面两个函数的理解,大家可以对照着理解。

    根据require函数的参数形式的不同,比如说直接引一个文件名 require("moduleA"),或者是路径require("./moduleA")等,查找方式会有一些变化:

    从 Y 路径的模块 require(X) 
    1. 如果 X 是一个核心模块,    
        a. 返回核心模块 //核心模块是指node.js下lib的内容    
        b. 结束 
    2. 如果 X 是以 './' 或 '/' 或 '../' 开头    
        a. 加载文件(Y + X)    
        b. 加载目录(Y + X) 
    3. 加载Node模块(X, dirname(Y)) // 导入一个NODE_MODULE,返回 
    4. 抛出 "未找到" // 上述都没找到,直接排出没找到的异常。 
    
    加载文件(X) 
    1. 如果 X 是一个文件,加载 X 作为 JavaScript 文本。结束 
    2. 如果 X.js 是一个文件,加载 X.js 作为 JavaScript 文本。结束 
    3. 如果 X.json 是一个文件,解析 X.json 成一个 JavaScript 对象。结束 
    4. 如果 X.node 是一个文件,加载 X.node 作为二进制插件。结束 
    
    加载目录(X) 
    1. 如果 X/package.json 是一个文件,    
        a. 解析 X/package.json,查找 "main" 字段    
        b. let M = X + (json main 字段)    
        c. 加载文件(M) 
    2. 如果 X/index.js 是一个文件,加载  X/index.js 作为 JavaScript 文本。结束 
    3. 如果 X/index.json 是一个文件,解析 X/index.json 成一个 JavaScript 对象。结束 
    4. 如果 X/index.node 是一个文件,加载  X/index.node 作为二进制插件。结束 
    
    加载Node模块(X, START) 
    1. let DIRS=NODE_MODULES_PATHS(START) //得到 node_module 文件目录 
    2. for each DIR in DIRS: // 遍历所有的路径 直到找到 x ,x 可能是 文件或者是目录    
        a. 加载文件(DIR/X)    
        b. 加载目录(DIR/X) 
    
    NODE_MODULES_PATHS(START) //具体NODE_MODULES文件目录算法 
    1. let PARTS = path split(START) 
    2. let I = count of PARTS - 1 
    3. let DIRS = [] 
    4. while I >= 0,    
        a. if PARTS[I] = "node_modules" CONTINUE    
        b. DIR = path join(PARTS[0 .. I] + "node_modules")    
        c. DIRS = DIRS + DIR    
        d. let I = I - 1 5. return DIRS

    1、Module._resolveLookupPaths() 方法

      1 // 'index.' character codes
      2 var indexChars = [ 105, 110, 100, 101, 120, 46 ];
      3 var indexLen = indexChars.length;
      4 //_resolveLookupPaths() 方法用来查找模块,返回一个数组,数组第一项为模块名称即request,数组第二项返回一个可能包含这个模块的文件夹路径数组
      5 //
      6 //处理了如下几种情况:
      7 // 1、是原生模块且不是内部模块
      8 // 2、如果路径不以"./" 或者'..'开头或者只有一个字符串,即是引用模块名的方式,即require('moduleA');
      9 //   2.1以 '/' 为前缀的模块是文件的绝对路径。 例如,require('/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。
     10 //   2.2以 './' 为前缀的模块是相对于调用 require() 的文件的。 也就是说,circle.js 必须和 foo.js 在同一目录下以便于 require('./circle') 找到它。
     11 //   2.3当没有以 '/'、'./' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录。
     12 Module._resolveLookupPaths = function(request, parent, newReturn) { //request 当前加载的模块名称,parent 父亲模块
     13 
     14   //NativeModule用于管理js模块,头部引入的。
     15   //NativeModule.nonInternalExists()用来判断是否是原生模块且不是内部模块,所谓内部模块就是指 lib/internal 文件目录下的模块,像fs等。
     16   if (NativeModule.nonInternalExists(request)) {
     17     debug('looking for %j in []', request);
     18 
     19     //满足 是原生模块且不是内部模块,也就是说是node.js下lib文件夹下的模块,
     20     //但不包含lib/internal 文件目录下的模块,并且newReturn 为true,则返回null ,
     21     //如果newReturn 为false 则返回[request, []]。
     22     return (newReturn ? null : [request, []]);
     23   }
     24 
     25   // Check for relative path
     26   // 检查相关路径
     27   // 如果路径不以"./"或者'..'开头或者只有一个字符串,即是引用模块名的方式,即require('moduleA');
     28   if (request.length < 2 ||
     29       request.charCodeAt(0) !== 46/*.*/ ||
     30       (request.charCodeAt(1) !== 46/*.*/ &&
     31        request.charCodeAt(1) !== 47/*/*/)) {
     32       //全局变量,在Module._initPaths 函数中赋值的变量,modulePaths记录了全局加载依赖的根目录
     33     var paths = modulePaths; 
     34 
     35     // 设置一下父亲的路径,其实就是谁导入了当前模块
     36     if (parent) {
     37       if (!parent.paths)
     38         paths = parent.paths = [];
     39       else
     40         paths = parent.paths.concat(paths);
     41     }
     42 
     43     // Maintain backwards compat with certain broken uses of require('.')
     44     // by putting the module's directory in front of the lookup paths.
     45     // 如果只有一个字符串,且是 .
     46     if (request === '.') {
     47       if (parent && parent.filename) {
     48         paths.unshift(path.dirname(parent.filename));
     49       } else {
     50         paths.unshift(path.resolve(request));
     51       }
     52     }
     53 
     54     debug('looking for %j in %j', request, paths);
     55 
     56     //直接返回
     57     return (newReturn ? (paths.length > 0 ? paths : null) : [request, paths]);
     58   }
     59 
     60   // with --eval, parent.id is not set and parent.filename is null
     61   // 处理父亲模块为空的情况
     62   if (!parent || !parent.id || !parent.filename) {
     63     // make require('./path/to/foo') work - normally the path is taken
     64     // from realpath(__filename) but with eval there is no filename
     65     // 生成新的目录, 在系统目录 modulePaths,当前目录 和 "node_modules" 作为候选的路径
     66     var mainPaths = ['.'].concat(Module._nodeModulePaths('.'), modulePaths);
     67 
     68     debug('looking for %j in %j', request, mainPaths);
     69     //直接返回
     70     return (newReturn ? mainPaths : [request, mainPaths]);
     71   }
     72 
     73   // Is the parent an index module?
     74   // We can assume the parent has a valid extension,
     75   // as it already has been accepted as a module.
     76   // 处理父亲模块是否为index模块,即 path/index.js 或者 X/index.json等 带有index字样的module
     77   const base = path.basename(parent.filename); // path.basename()返回路径中的最后一部分
     78   var parentIdPath;
     79   if (base.length > indexLen) {
     80     var i = 0;
     81 
     82     //检查 引入的模块名中是否有 "index." 字段,如果有, i === indexLen。
     83     for (; i < indexLen; ++i) {
     84       if (indexChars[i] !== base.charCodeAt(i))
     85         break;
     86     }
     87 
     88     // 匹配 "index." 成功,查看是否有多余字段以及剩余部分的匹配情况
     89     if (i === indexLen) {
     90       // We matched 'index.', let's validate the rest
     91       for (; i < base.length; ++i) {
     92         const code = base.charCodeAt(i);
     93 
     94         // 如果模块名中有  除了 _, 0-9,A-Z,a-z 的字符 则跳出,继续下一次循环
     95         if (code !== 95/*_*/ &&
     96             (code < 48/*0*/ || code > 57/*9*/) &&
     97             (code < 65/*A*/ || code > 90/*Z*/) &&
     98             (code < 97/*a*/ || code > 122/*z*/))
     99           break;
    100       }
    101 
    102 
    103       if (i === base.length) {
    104         // Is an index module
    105         parentIdPath = parent.id;
    106       } else {
    107         // Not an index module
    108         parentIdPath = path.dirname(parent.id); //path.dirname() 返回路径中代表文件夹的部分
    109       }
    110     } else {
    111       // Not an index module
    112       parentIdPath = path.dirname(parent.id);
    113     }
    114   } else {
    115     // Not an index module
    116     parentIdPath = path.dirname(parent.id);
    117   }
    118 
    119   //拼出绝对路径
    120   //path.resolve([from ...], to) 将 to 参数解析为绝对路径。
    121   //eg:path.resolve('/foo/bar', './baz') 输出'/foo/bar/baz'
    122   var id = path.resolve(parentIdPath, request);  
    123 
    124   // make sure require('./path') and require('path') get distinct ids, even
    125   // when called from the toplevel js file
    126   // 确保require('./path')和require('path')两种形式的,获得不同的 ids
    127   if (parentIdPath === '.' && id.indexOf('/') === -1) {
    128     id = './' + id;
    129   }
    130 
    131   debug('RELATIVE: requested: %s set ID to: %s from %s', request, id,
    132         parent.id);
    133   //path.dirname() 返回路径中代表文件夹的部分
    134   var parentDir = [path.dirname(parent.filename)]; 
    135 
    136   debug('looking for %j in %j', id, parentDir);
    137 
    138   // 当我们以"./" 等方式require时,都是以当前引用他的模块,也就是父亲模块为对象路径的
    139   return (newReturn ? parentDir : [id, parentDir]);
    140 };

    2、Module._findPath() 方法

      1 var warned = false;
      2 //_findPath用于从可能的路径中确定哪一个路径为真,并且添加到缓存中
      3 //参数request 当前加载的模块名称,
      4 //paths ,Module._resolveLookupPaths()函数返回一个数组[id , paths],即模块可能在的所有路径,
      5 // /* isMain */ false  是不是主入口文件
      6 Module._findPath = function(request, paths, isMain) {
      7 
      8   //path.isAbsolute()判断参数 path 是否是绝对路径。
      9   if (path.isAbsolute(request)) {  
     10     paths = [''];
     11   } else if (!paths || paths.length === 0) {
     12     return false;
     13   }
     14 
     15 
     16   var cacheKey = request + '\x00' +
     17                 (paths.length === 1 ? paths[0] : paths.join('\x00'));
     18   var entry = Module._pathCache[cacheKey];
     19 
     20   //判断是否在缓存中,如果有则直接返回
     21   if (entry)
     22     return entry;
     23 
     24   //如果不在缓存中,则开始查找
     25   var exts;
     26   // 当前加载的模块名称大于0位并且最后一位是 / ,即是否有后缀的目录斜杠
     27   var trailingSlash = request.length > 0 &&
     28                       request.charCodeAt(request.length - 1) === 47/*/*/;
     29 
     30   // For each path
     31   // 循环每一个可能的路径paths
     32   for (var i = 0; i < paths.length; i++) {
     33 
     34     // Don't search further if path doesn't exist
     35     // 如果路径存在就继续执行,不存在就继续检验下一个路径 stat 获取路径状态
     36     const curPath = paths[i];
     37     if (curPath && stat(curPath) < 1) continue;
     38     var basePath = path.resolve(curPath, request); //生成绝对路径
     39     var filename;
     40 
     41     //stat 头部定义的函数,用来获取路径状态,判断路径类型,是文件还是文件夹
     42     var rc = stat(basePath);
     43     //如果没有后缀的目录斜杠,那么就有可能是文件或者是文件夹名
     44     if (!trailingSlash) {
     45       // 若是文件
     46       if (rc === 0) {  // File.
     47 
     48         // 如果是使用模块的符号路径而不是真实路径,并且不是主入口文件
     49         if (preserveSymlinks && !isMain) {  
     50           filename = path.resolve(basePath);
     51         } else {
     52           filename = toRealPath(basePath); //获取当前执行文件的真实路径
     53         }
     54 
     55       // 若是目录
     56       } else if (rc === 1) {  // Directory.
     57         if (exts === undefined)
     58           //目录中是否存在 package.json
     59           //通过package.json文件,返回相应路径
     60           exts = Object.keys(Module._extensions);
     61         filename = tryPackage(basePath, exts, isMain);
     62       }
     63 
     64       // 如果尝试了上面都没有得到filename 匹配所有扩展名进行尝试,是否存在
     65       if (!filename) {
     66         // try it with each of the extensions
     67         if (exts === undefined)
     68           exts = Object.keys(Module._extensions);
     69         // 该模块文件加上后缀名js .json .node进行尝试,是否存在
     70         filename = tryExtensions(basePath, exts, isMain);
     71       }
     72     }
     73 
     74     // 如果仍然没有得到filename,并且路径类型是文件夹
     75     if (!filename && rc === 1) {  // Directory.
     76       if (exts === undefined)
     77         // 目录中是否存在 package.json
     78         // 通过package.json文件,返回相应路径
     79         exts = Object.keys(Module._extensions);
     80       filename = tryPackage(basePath, exts, isMain);
     81     }
     82 
     83     // 如果仍然没有得到filename,并且路径类型是文件夹
     84     if (!filename && rc === 1) {  // Directory.
     85       // try it with each of the extensions at "index"
     86       // 是否存在目录名 + index + 后缀名
     87       // 尝试 index.js index.json index.node
     88       if (exts === undefined)
     89         exts = Object.keys(Module._extensions);
     90 
     91       //tryExtensions()头部定义方法,用来检查文件加上js node json后缀是否存在
     92       filename = tryExtensions(path.resolve(basePath, 'index'), exts, isMain);
     93     }
     94 
     95 
     96     if (filename) {
     97       // Warn once if '.' resolved outside the module dir
     98       if (request === '.' && i > 0) {
     99         if (!warned) {
    100           warned = true;
    101           process.emitWarning(
    102             'warning: require(\'.\') resolved outside the package ' +
    103             'directory. This functionality is deprecated and will be removed ' +
    104             'soon.',
    105             'DeprecationWarning', 'DEP0019');
    106         }
    107       }
    108 
    109       // 将找到的文件路径存入返回缓存,然后返回
    110       Module._pathCache[cacheKey] = filename;
    111       return filename;
    112     }
    113   }
    114 
    115   // 所以从这里可以看出,对于具体的文件的优先级:
    116   // 1. 具体文件。
    117   // 2. 加上后缀。
    118   // 3. package.json
    119   // 4  index加上后缀
    120   // 可能的路径以当前文件夹,nodejs系统文件夹和node_module中的文件夹为候选,以上述顺序找到任意一个,
    121   // 就直接返回
    122 
    123   // 没有找到文件,返回false
    124   return false;
    125 };

    Module._load 中还调用了 tryModuleLoad() 方法

     1 function tryModuleLoad(module, filename) {
     2   var threw = true;
     3 
     4   //try catch一下,如果装载失败,就会从cache中将这个模块删除。
     5   try {
     6 
     7     //做真正的导入模块的操作
     8     module.load(filename);
     9     threw = false;
    10   } finally {
    11     if (threw) {
    12       delete Module._cache[filename];
    13     }
    14   }
    15 }

    tryModuleLoad() 中调用了 Module.prototype.load() 方法

     1 // Given a file name, pass it to the proper extension handler.
     2 // 指定一个文件名,导入模块,调用适当扩展处理函数,当前主要是js,json,和node
     3 Module.prototype.load = function(filename) {
     4   debug('load %j for module %j', filename, this.id);
     5 
     6   assert(!this.loaded); //断言 确保当前模块没有被载入
     7   this.filename = filename; // 赋值当前模块的文件名
     8 
     9   // Module._nodeModulePaths主要决定paths参数的值的方法。获取node_modules文件夹所在路径。
    10   // path.dirname() 方法返回一个 path 的目录名 path.dirname('/foo/bar/baz/asdf/quux')
    11   // 返回: '/foo/bar/baz/asdf'
    12   this.paths = Module._nodeModulePaths(path.dirname(filename));
    13 
    14   //当前文件的后缀
    15   var extension = path.extname(filename) || '.js';
    16 
    17   //如果没有后缀,默认为 .js
    18   if (!Module._extensions[extension]) extension = '.js';
    19 
    20   //根据不同的后缀,执行不同的函数
    21   Module._extensions[extension](this, filename);
    22   this.loaded = true;
    23 };

    Module.prototype.load() 中调用了 Module._nodeModulePaths() 和 Module._extensions 方法

    1、Module._nodeModulePaths() 根据操作系统的不同,返回不同的函数

     1 //path 模块的默认操作会根据 Node.js 应用程序运行的操作系统的不同而变化。 
     2 //比如,当运行在 Windows 操作系统上时,path 模块会认为使用的是 Windows 风格的路径。
     3 //例如,对 Windows 文件路径 C:\temp\myfile.html 使用 path.basename() 函数,
     4 //运行在 POSIX 上与运行在 Windows 上会产生不同的结果:
     5 //在 POSIX 上:
     6 //path.basename('C:\\temp\\myfile.html');
     7 // 返回: 'C:\\temp\\myfile.html'
     8 //
     9 // 在 Windows 上:
    10 //path.basename('C:\\temp\\myfile.html');
    11 // 返回: 'myfile.html'
    12 //
    13 // 以下就是根据不同的操作系统返回不同的路径格式 ,具体可以了解http://nodejs.cn/api/path.html
    14 //
    15 //
    16 // Module._nodeModulePaths主要决定paths参数的值的方法。获取node_modules文件夹所在路径。
    17 // 'node_modules' character codes reversed
    18 var nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ];
    19 var nmLen = nmChars.length;
    20 if (process.platform === 'win32') {
    21   // 'from' is the __dirname of the module.
    22   Module._nodeModulePaths = function(from) {
    23     // guarantee that 'from' is absolute.
    24     from = path.resolve(from);
    25 
    26     // note: this approach *only* works when the path is guaranteed
    27     // to be absolute.  Doing a fully-edge-case-correct path.split
    28     // that works on both Windows and Posix is non-trivial.
    29 
    30     // return root node_modules when path is 'D:\\'.
    31     // path.resolve will make sure from.length >=3 in Windows.
    32     if (from.charCodeAt(from.length - 1) === 92/*\*/ &&
    33         from.charCodeAt(from.length - 2) === 58/*:*/)
    34       return [from + 'node_modules'];
    35 
    36     const paths = [];
    37     var p = 0;
    38     var last = from.length;
    39     for (var i = from.length - 1; i >= 0; --i) {
    40       const code = from.charCodeAt(i);
    41       // The path segment separator check ('\' and '/') was used to get
    42       // node_modules path for every path segment.
    43       // Use colon as an extra condition since we can get node_modules
    44       // path for dirver root like 'C:\node_modules' and don't need to
    45       // parse driver name.
    46       if (code === 92/*\*/ || code === 47/*/*/ || code === 58/*:*/) {
    47         if (p !== nmLen)
    48           paths.push(from.slice(0, last) + '\\node_modules');
    49         last = i;
    50         p = 0;
    51       } else if (p !== -1) {
    52         if (nmChars[p] === code) {
    53           ++p;
    54         } else {
    55           p = -1;
    56         }
    57       }
    58     }
    59 
    60     return paths;
    61   };
    62 } else { // posix
    63   // 'from' is the __dirname of the module.
    64   Module._nodeModulePaths = function(from) {
    65     // guarantee that 'from' is absolute.
    66     from = path.resolve(from);
    67     // Return early not only to avoid unnecessary work, but to *avoid* returning
    68     // an array of two items for a root: [ '//node_modules', '/node_modules' ]
    69     if (from === '/')
    70       return ['/node_modules'];
    71 
    72     // note: this approach *only* works when the path is guaranteed
    73     // to be absolute.  Doing a fully-edge-case-correct path.split
    74     // that works on both Windows and Posix is non-trivial.
    75     const paths = [];
    76     var p = 0;
    77     var last = from.length;
    78     for (var i = from.length - 1; i >= 0; --i) {
    79       const code = from.charCodeAt(i);
    80       if (code === 47/*/*/) {
    81         if (p !== nmLen)
    82           paths.push(from.slice(0, last) + '/node_modules');
    83         last = i;
    84         p = 0;
    85       } else if (p !== -1) {
    86         if (nmChars[p] === code) {
    87           ++p;
    88         } else {
    89           p = -1;
    90         }
    91       }
    92     }
    93 
    94     // Append /node_modules to handle root paths.
    95     paths.push('/node_modules');
    96 
    97     return paths;
    98   };
    99 }

    2、Module._extensions 方法

     1 // 根据不同的文件类型,三种后缀,Node.js会进行不同的处理和执行
     2 // 对于.js的文件会,先同步读取文件,然后通过module._compile解释执行。
     3 // 对于.json文件的处理,先同步的读入文件的内容,无异常的话直接将模块的exports赋值为json文件的内容
     4 // 对于.node文件的打开处理,通常为C/C++文件。
     5 // Native extension for .js
     6 Module._extensions['.js'] = function(module, filename) {
     7   // 同步读取文件
     8   var content = fs.readFileSync(filename, 'utf8');
     9 
    10   // internalModule.stripBOM()剥离 utf8 编码特有的BOM文件头,
    11   // 然后通过module._compile解释执行
    12   module._compile(internalModule.stripBOM(content), filename);
    13 };
    14 
    15 
    16 // Native extension for .json
    17 Module._extensions['.json'] = function(module, filename) {
    18   // 同步的读入文件的内容
    19   var content = fs.readFileSync(filename, 'utf8');
    20   try {
    21     // internalModule.stripBOM()剥离 utf8 编码特有的BOM文件头,
    22     // 然后将模块的exports赋值为json文件的内容
    23     module.exports = JSON.parse(internalModule.stripBOM(content));
    24   } catch (err) {
    25     // 异常处理
    26     err.message = filename + ': ' + err.message;
    27     throw err;
    28   }
    29 };
    30 
    31 
    32 //Native extension for .node
    33 Module._extensions['.node'] = function(module, filename) {
    34   // 对于.node文件的打开处理,通常为C/C++文件。
    35   return process.dlopen(module, path._makeLong(filename));
    36 };

    针对 .js 后缀的,在 Module._extensions 还调用了 module._compile() 方法

      1 // Resolved path to process.argv[1] will be lazily placed here
      2 // (needed for setting breakpoint when called with --debug-brk)
      3 var resolvedArgv;
      4 // Run the file contents in the correct scope or sandbox. Expose
      5 // the correct helper variables (require, module, exports) to
      6 // the file.
      7 // Returns exception, if any.
      8 // 此方法用于模块的编译。
      9 // 参数content 主要是模块js文件的主要内容,filename 是js文件的文件名
     10 Module.prototype._compile = function(content, filename) {
     11   // Remove shebang
     12   // Shebang(也称为 Hashbang )是一个由井号和叹号构成的字符序列 #!
     13   var contLen = content.length;
     14   if (contLen >= 2) {
     15     // 如果content 开头有Shebang
     16     if (content.charCodeAt(0) === 35/*#*/ &&
     17         content.charCodeAt(1) === 33/*!*/) {
     18       if (contLen === 2) {
     19         // Exact match
     20         content = '';
     21       } else {
     22         // Find end of shebang line and slice it off
     23         // 找到以shebang开头的句子的结尾,并将其分开,留下剩余部分 赋值给content
     24         var i = 2;
     25         for (; i < contLen; ++i) {
     26           var code = content.charCodeAt(i);
     27           if (code === 10/*\n*/ || code === 13/*\r*/)
     28             break;
     29         }
     30         if (i === contLen)
     31           content = '';
     32         else {
     33           // Note that this actually includes the newline character(s) in the
     34           // new output. This duplicates the behavior of the regular expression
     35           // that was previously used to replace the shebang line
     36           content = content.slice(i);
     37         }
     38       }
     39     }
     40   }
     41 
     42   // create wrapper function
     43   // Module.wrap头部引入,主要用来给content内容包装头尾,类似于
     44 //   (function (exports, require, module, __filename, __dirname) {
     45 //         -----模块源码 content-----
     46 //    });
     47   var wrapper = Module.wrap(content);
     48 
     49 // 包装好的文本就可以送到vm中执行了,这部分就应该是v8引擎的事情,
     50 // runInThisContext将被包装后的源字符串转成可执行函数,runInThisContext的作用,类似eval
     51   var compiledWrapper = vm.runInThisContext(wrapper, {
     52     filename: filename,
     53     lineOffset: 0,
     54     displayErrors: true
     55   });
     56 
     57   var inspectorWrapper = null;
     58   // 处理debug模式,
     59   if (process._debugWaitConnect && process._eval == null) {
     60     if (!resolvedArgv) {
     61       // we enter the repl if we're not given a filename argument.
     62       if (process.argv[1]) {
     63         resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
     64       } else {
     65         resolvedArgv = 'repl';
     66       }
     67     }
     68 
     69     // Set breakpoint on module start
     70     if (filename === resolvedArgv) {
     71       delete process._debugWaitConnect;
     72       inspectorWrapper = getInspectorCallWrapper();
     73       if (!inspectorWrapper) {
     74         const Debug = vm.runInDebugContext('Debug');
     75         Debug.setBreakPoint(compiledWrapper, 0, 0);
     76       }
     77     }
     78   }
     79 
     80   // 获取当前的文件的路径
     81   var dirname = path.dirname(filename);
     82 
     83   //生成require方法
     84   var require = internalModule.makeRequireFunction(this);
     85 
     86   //依赖模块
     87   var depth = internalModule.requireDepth;
     88   if (depth === 0) stat.cache = new Map();
     89   var result;
     90 
     91   //直接调用content经过包装后的wrapper函数,将module模块中的exports,生成的require,
     92   //this也就是新创建的module,filename, dirname作为参数传递给模块
     93   //类似于
     94   //(function (exports, require, module, __filename, __dirname) {
     95 //       -----模块源码 content-----
     96 //  })( this.exports, require, this, filename, dirname);
     97   // 这就是为什么我们可以直接在module文件中,直接访问exports, module, require函数的原因
     98   if (inspectorWrapper) {
     99     result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
    100                               require, this, filename, dirname);
    101   } else {
    102     result = compiledWrapper.call(this.exports, this.exports, require, this,
    103                                   filename, dirname);
    104   }
    105   if (depth === 0) stat.cache = null;
    106   return result;
    107 };

    Module.prototype._compile 中调用了 Module.wrap 这个方法就是用了给 content 包装的主要函数, 它来自头部的引用:

     1 //Module.wrapper和Module.wrap的方法写在下面,
     2 //给传入进去的script也就是咱们的content --js文件内容套了一个壳,使其最后变成类似于如下的样子:
     3 //
     4 //(function (exports, require, module, __filename, __dirname) {
     5 //         -----模块源码-----
     6 // });
     7 //
     8 // NativeModule.wrap = function(script) {
     9 //     return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
    10 // };
    11 
    12 // NativeModule.wrapper = [
    13 //     '(function (exports, require, module, __filename, __dirname) { ',
    14 //     '\n});'
    15 // ];
    16 Module.wrapper = NativeModule.wrapper;
    17 Module.wrap = NativeModule.wrap;
    module.js 中还定义了一些其它的方法,在这里没有写出来,像 stat、readPackage、tryPackage、头部引入的一些方法等,大家可以 从我的github(https://github.com/JiayiLi/node.js-module)上 clone 下来,放到 IDE 里跟着调用顺序一步一步的看,都有详细的注释。
     
    根据函数调用顺序,总体梳理一下
     

                                                                           (图一) 

    现在咱们再看这个图,梳理一下刚才的代码,就清晰多了。 

     

                                                        (图二)

    最后,还有个问题 lib 目录下的模块文件,像 module.js 也没有定义 require ,module,exports 这些变量,他们是如何使用的呢?
    这是因为在引入核心模块的时候也进行了头尾包装的过程。这里就要提到 lib/internal 文件夹下的 bootstrap_node.js,属于 node 启动文件。

    在 bootstrap_node.js 中定义了一个 NativeModule 对象,用于加载核心模块,如 module.js、http.js 等即 lib 文件夹下的 排除 lib/internal 目录下的 js 模块。

    在这个 NativeModule 对象中也定义了 require 方法,compile 方法、wrap 方法(用于包装头尾)等 都和上面的 module.js 中的相应的方法意思是一样的,可以下载源码了解一下。

    结论就是,node.js 通过 NativeModule 来对 module.js 、fs.js 等核心模块进行包装加载,所以它们里面也可以使用 require。 
     
    最后还是建议从我的 github(https://github.com/JiayiLi/node.js-module)上 clone 下来 ,放到 ide 里,按照函数的调用一个一个看,看的过程中也对照着图一、图二理清思路。 
     
     
     
    ------------- 学会的知识也要时常review ------------
     
     
     
     
     
     
  • 相关阅读:
    Basic INFO InstallShield的版本区分
    月光博客 从软件保护到软件授权
    Basic INFO InstallShield Professional for AdminStudio版本中如何打开InstallScript工程类型
    Basic INFO InstallScript脚本编程的限制
    从软件加密到软件授权保护
    月光博客 再谈软件保护中软加密和硬加密的安全强度
    INFO:Visual Studio 11 Beta版本中去除对Visual Studio Installer的支持,只保留InstallShield LE版本
    android 抓包分析
    android 巧用 dimerns 分辨率
    android 抓包 2
  • 原文地址:https://www.cnblogs.com/lijiayi/p/js_node_module.html
Copyright © 2020-2023  润新知