CommonJS 模块规范以及模块原理
规范主要内容
-
定义模块
根据CommonJS规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性
-
模块输出
模块只有一个出口,module.exports对象,需要把模块希望输出的内容放入该对象
-
加载模块
加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象
分析:
由于上面的CommonJS规范中的require是同步的,模块系统需要同步读取模块文件内容,并编译执行以得到模块接口,这在服务器端实现很简单,但是由于js脚本天生异步,因此在浏览器端实现问题会很多。
1. 什么是模块化
具有文件作用域以及通信规则(导入、导出)则称其具有模块化
在node中的js有个重要的概念:模块系统
- 模块作用域
- 使用require方法用来加载模块
- 使用exports接口对象用来导出模块中的成员
2.common JS模块规范
1.加载require
-
语法
var 自定义变量名称 = require('模块');
-
作用
- 执行被加载模块中的代码
- 返回被加载模块中的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上面的数据,因此此时导出的仍然是空对象
也即以下的场景:
而如果对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引用指向基本数据类型或者复杂数据类型即可
总结:
-
导出多个成员
-
挂载在exports对象上(此时的exports引用的module.exports)
exports.add = 123; exports.minus = 456;
-
指向多数据对象
module.exports = { add: 123, minus: 456 };// 这个地方不能用exports来引用这个对象,不然会改变exports引用,但是最后导出的module.exports对象并没有获取这个值
-
-
导出单个成员
module.exports = 'hello';
4.require标识符分析
当require一个文件的时候,会按照以下顺序执行查找
-
如果是路径形式的模块
./ 当前目录,不可省略 ../ 上一级目录,不可省略 /xxx 几乎不用 e:/a/index.js 几乎不用 其中首位的/ 在这里表示的是当前文件模块所属磁盘根路径
也即: require('./foo.js'); // 或者省略.js文件 默认访问为.js文件 require('./foo')
而以上加载方式作为自定义模块的加载方式
-
如果是非路径形式的模块标识
查看是否是核心模块。由于核心模块文件已经被编制到了二进制文件中,也只需要按照名字来加载就可以了。
require('http');
-
既不是核心模块,也不是路径形式的模块,则可能是第三方模块
-
凡是第三方模块都必须通过npm来下载
-
使用的时候可以通过require(‘包名’);的方式进行加载才可以使用
-
不可能任何一个第三棒包和核心模块的名字是一样的情况产生
-
第三方模块查找规则(以require(‘atr-template’))为例:
-
先找到当前文件所处目录中的node_modules目录
-
node_modules/art-template
-
node_modules/art-template/package.json文件
-
node_modules/art-template/package.json文件中的main属性
-
由于main属性中记录了art-template的入口模块
-
然后根据main属性加载使用这个第三方模块(一般是js文件)
-
如果package.json文件不存在或者main指定的入口模块找不到
- node会自动找该目录下的Index.js文件作为备选入口文件
-
如果以上所有任何一个条件都不成立,则会进入上一级目录中的node_modules目录查找,如果上一级还没有,逐级向上查找,如果直到当前磁盘根目录还找不到,最后报错:can not find module xxx
-
注意:
一个项目有且只有一个node_modules,放于项目根目录中,这样项目中的所有子目录中的代码都可以加载到第三方包,不会出现多个node_modules
-
-
5.require加载规则
-
优先从缓存加载
考虑以下场景
// 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
-
根据require标识符分析的方式查找模块