CommonJS 模块规范


规范主要内容

  • 定义模块

    根据CommonJS规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性

  • 模块输出

    模块只有一个出口,module.exports对象,需要把模块希望输出的内容放入该对象

  • 加载模块

    加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象

分析:

由于上面的CommonJS规范中的require是同步的,模块系统需要同步读取模块文件内容,并编译执行以得到模块接口,这在服务器端实现很简单,但是由于js脚本天生异步,因此在浏览器端实现问题会很多。

1. 什么是模块化

具有文件作用域以及通信规则(导入、导出)则称其具有模块化

扫描二维码关注公众号,回复: 12852268 查看本文章

在node中的js有个重要的概念:模块系统

  • 模块作用域
  • 使用require方法用来加载模块
  • 使用exports接口对象用来导出模块中的成员
2.common JS模块规范
1.加载require
  1. 语法

    var 自定义变量名称 = require('模块');
    
  2. 作用

    • 执行被加载模块中的代码
    • 返回被加载模块中的exports导出对象
2.导出exports

Node 中是模块作用域,默认文件中的所有对象只在当前文件模块有效,对于想要被其他模块访问的成员,需要将公开的成员都挂载到exports接口对象中

  • 导出多个成员:挂载到exports对象中

    exports.str = 'hello';
    exports.add = function() {
          
          
        return 1;
    }
    
    // 或者:
    module.exports = {
          
          
        str: 'hello',
        arr: []
    }
    
  • 导出单个成员:直接赋值

    module.exports = 'hello';
    // 由于是赋值操作,后者会覆盖前者
    module.exports = 123
    
3.模块原理(exports导出对象实质)

node中每一个模块内部都有一个module对象,该module对象中有一个成员:exports对象

let module = {
    
    
    exports: {
    
    };
}

并且默认在模块末尾代码处有以下代码:

return module.exports;

所以可以看出每个被加载的模块实际上导出的是module.exports对象,而在其对象上挂载数据一般通过

module.exports.data = 'hello';
module.exports.str = 123;

其他模块加载该模块便可以获取到该模块导出的对象上挂载的数据,而后期node为了方便书写的考虑,定义了一个对象exports来引用module.exports,即

var exports = module.exports; 

而记住,exports仅仅是module.exports的引用,被加载模块导出的是module.exports,因此如果改变exports的引用,再挂载值,此时并不会导出改变exports引用之后挂载的值

exports = {
    
    };// 改变引用
exports.add = 123;// 挂载新数据
// 此时
exports == module.exports; // false
return module.exports;// 但是并没有修改module.exports上面的数据,因此此时导出的仍然是空对象

也即以下的场景:

border_01

而如果对module.exports改变即的引用会发生什么呢?由于最开始exports和module.exports的引用是一致的,后期修改了module.exports的引用,但是请记住一点:模块最后导出的是module.exports对象,因此不管怎么改变,导出的数据都是module.exports,而exports仅仅是对module.exports的引用,用于简写用途的。因此查看以下代码:

module.exports  = {
    
    
    add: 1
}
exports = {
    
    
    foo: 2
}
// 最后导出的数据为{add:1}
// -------------------------------------
module.exports = 'hello';// 此时导出的仅仅是一个字符串

从以上代码可以得出:模块导出的数据全由module.exports来决定,如果要导出多个成员,则可以将数据挂载在module.exports对象上或者改变引用指向一个多数据对象;如果要导出单个成员,则直接将module.exports引用指向基本数据类型或者复杂数据类型即可

总结:

  1. 导出多个成员

    • 挂载在exports对象上(此时的exports引用的module.exports)

      exports.add = 123;
      exports.minus = 456;
      
    • 指向多数据对象

      module.exports = {
              
              
          add: 123,
          minus: 456
      };// 这个地方不能用exports来引用这个对象,不然会改变exports引用,但是最后导出的module.exports对象并没有获取这个值
      
  2. 导出单个成员

    module.exports = 'hello';
    
4.require标识符分析

当require一个文件的时候,会按照以下顺序执行查找

  1. 如果是路径形式的模块

    ./ 当前目录,不可省略
    ../ 上一级目录,不可省略
    /xxx 几乎不用
    e:/a/index.js 几乎不用
    其中首位的/ 在这里表示的是当前文件模块所属磁盘根路径
    
    也即:
    require('./foo.js');
    // 或者省略.js文件 默认访问为.js文件
    require('./foo')
    

    而以上加载方式作为自定义模块的加载方式

  2. 如果是非路径形式的模块标识

    查看是否是核心模块。由于核心模块文件已经被编制到了二进制文件中,也只需要按照名字来加载就可以了。

    require('http');
    
  3. 既不是核心模块,也不是路径形式的模块,则可能是第三方模块

    1. 凡是第三方模块都必须通过npm来下载

    2. 使用的时候可以通过require(‘包名’);的方式进行加载才可以使用

    3. 不可能任何一个第三棒包和核心模块的名字是一样的情况产生

    4. 第三方模块查找规则(以require(‘atr-template’))为例:

      1. 先找到当前文件所处目录中的node_modules目录

      2. node_modules/art-template

      3. node_modules/art-template/package.json文件

      4. node_modules/art-template/package.json文件中的main属性

      5. 由于main属性中记录了art-template的入口模块

      6. 然后根据main属性加载使用这个第三方模块(一般是js文件)

      7. 如果package.json文件不存在或者main指定的入口模块找不到

        1. node会自动找该目录下的Index.js文件作为备选入口文件
      8. 如果以上所有任何一个条件都不成立,则会进入上一级目录中的node_modules目录查找,如果上一级还没有,逐级向上查找,如果直到当前磁盘根目录还找不到,最后报错:can not find module xxx

      9. 注意

        一个项目有且只有一个node_modules,放于项目根目录中,这样项目中的所有子目录中的代码都可以加载到第三方包,不会出现多个node_modules

5.require加载规则
  1. 优先从缓存加载

    考虑以下场景

    // a.js
    require('./b');
    require('./c');
    // b.js
    console.log('b.js被加载');
    require('./c');
    // c.js
    console.log('c.js被加载');
    

    执行a.js文件,执行顺序是:加载并执行b.js,打印b.js被加载,在b.js中,加载并执行c.js,打印c.js被加载,b.js执行完毕,回到a.js,此时并不会再次加载c.js文件。也即打印结果为:

    b.js被加载
    c.js被加载
    // 而不是
    b.js被加载
    c.js被加载
    c.js被加载
    

    原因:当加载了b.js的时候,以及加载了c.js,并将导出的接口数据放入到了缓存中,当a.js再次想要加载c.js的时候,先从缓存中查找有没有该模块,有则不再加载运行,直接从缓存中加载已缓存的接口数据而不会再次运行c.js文件,所以以下代码变得合理:

    // a.js
    require('./b');
    let cExports = require('./c');
    console.log(xExports);
    // b.js
    console.log('b.js被加载');
    let cExports = require('./c');
    console.log(cExports);
    // c.js
    console.log('c.js被加载');
    module.exports = 'hello';
    
    // 结果为:
    b.js被加载
    c.被加载
    hello
    hello
    
  2. 根据require标识符分析的方式查找模块

猜你喜欢

转载自blog.csdn.net/chen__cheng/article/details/114652393