zoukankan      html  css  js  c++  java
  • 【 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 ------------
     
     
     
     
     
     
  • 相关阅读:
    AJAX异步传输——以php文件传输为例
    js控制json生成菜单——自制菜单(一)
    vs2010中关于HTML控件与服务器控件分别和js函数混合使用的问题
    SQL数据库连接到服务器出错——无法连接到XXX
    PHP错误:Namespace declaration statement has to be the very first statement in the script
    【LeetCode】19. Remove Nth Node From End of List
    【LeetCode】14. Longest Common Prefix
    【LeetCode】38. Count and Say
    【LeetCode】242. Valid Anagram
    【LeetCode】387. First Unique Character in a String
  • 原文地址:https://www.cnblogs.com/lijiayi/p/js_node_module.html
Copyright © 2011-2022 走看看