对于JS自己而言,他的规范是薄弱的,具备如下不足:html
CommonJS 是一种使用普遍的JavaScript模块化规范,核心思想是经过require方法来同步地加载依赖的其余模块,经过 module.exports 导出须要暴露的接口。前端
在CommonJS规范中,存在require()
方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。 模块引用的示例代码以下:node
const path = require("path");
复制代码
上下文提供了exports
对象用于导出当前模块的方法或者变量,而且它是惟一导出的出口。json
在模块中,还存在一个module
对象,它表明模块自身,而exports
是module
的属性。数组
在Node中,一个文件就是一个模块,将方法挂载在exports
对象做为属性便可定义导出的方式,以下:浏览器
// math.js
exports.add = function(){
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while(i < l){
sum += args[i++]
}
return sum;
}
复制代码
在另一个文件中,咱们经过require()
方法引入模块后,就能调用定义的属性或方法:缓存
var math = require("math");
exports.increment = function(val){
return math.add(val, 1)
}
复制代码
模块标识其实就是传递给require()
方法的参数,他必须是符合小驼峰命名的字符串,或者以.
、..
开头的相对路径,或者绝对路径。模块化
CommonJS的构建的这套模块导出和引入机制使得用户彻底不考虑变量污染,命名空间等方案与此相比相形见绌。函数
http
、fs
、path
、events
等模块,是Node提供的模块,这些模块在Node源代码的编译过程当中被编译成二进制。在Node进程启动时,部分原生代码就被直接加载进内存中,因此原生模块引入时,文件定位和编译执行这个两个步骤能够省略掉,而且在路径分析中优先判断, 因此加载速度最快。原生模块经过名称来加载。性能
在硬盘的某个位置,在运行时动态加载,须要完成的路径分析、文件定位、编译执行过程,速度比原生模块慢。
文件模块经过名称或路径来加载,文件模块的后缀有三种,以下
require
函数只指定名称则视为从node_modules
下面加载文件,这样的话你能够移动模块而不须要修改引用的模块路径module.paths
和全局目录全局目录
window若是在环境变量中设置了NODE_PATH
变量,并将变量设置为一个有效的磁盘目录,require在本地找不到此模块时向在此目录下找这个模块。
UNIX操做系统中会从 $HOME/.node_modules
$HOME/.node_libraries
目录下寻找
Node对引入过的模块都会进行缓存,以减小二次引入时的开销,与前端浏览器缓存静态脚本不一样,浏览器仅缓存文件,而Node缓存的是编译和执行后的对象。
不管是原生模块仍是文件模块等, require()
方法对相同模块的加载都一概采用缓存优先的方式,这是第一优先级的。
缓存优先策略,以下图:
module.paths
模块路径console.log(module.paths)
[ '/Users/**/Documents/framework/article/node中的CommonJS/node_modules',
'/Users/****/Documents/framework/article/node_modules',
'/Users/**/Documents/framework/node_modules',
'/Users/**/Documents/node_modules',
'/Users/**/node_modules',
'/Users/node_modules',
'/node_modules' ]
复制代码
在加载过程当中,Node会逐个尝试module.paths
中的路径,直到找到目标文件为止。因此当前文件的路径约深,模块查找耗时越多。因此第三方模块加载速度最慢。
尝试过程当中须要调用fs模块同步阻塞判断文件是否存在,由于是单线程,会引发性能问题。
诀窍是: 若是是.node和.json文件,传递时带上扩展名.
require()
分析文件扩展名以后,可能没有查找到对应文件,但却获得一个目录,此时Node会将该目录当作一个包来处理。首先,Node会在当前目录下查找package.json
,从中取出main
属性指定的文件进行定位。 若是文件缺乏扩展名,将会进入扩展名分析的步骤。 若是main
属性指定的文件名错误,或者根本没有package.json
,Node会将index
当作默认文件名,而后依次查找index.js
、index.json
、index.node
。
若是在目录分析中没有定位成功任何文件,则进入下一个模块路径进行查找。若是模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。
以下图:
module
的属性在Node中,每一个文件模块都是一个对象,定义以下:
console.log(module)
/* Module { id: '.', exports: {}, parent: null, filename: '/Users/.../article/015_node中的CommonJS/tempCodeRunnerFile.js', loaded: false, children: [], paths: [ '/Users/.../article/015_node中的CommonJS/node_modules', '/Users/.../article/node_modules', '/Users/.../node_modules', '/Users/.../node_modules', '/Users/.../node_modules', '/Users/node_modules', '/node_modules' ] } */
复制代码
编译和执行是引入文件模块的最后一个阶段。定位到具体文件后,Node会建一个模块对象,而后根据路径载入并编译。对于不一样的文件扩展名,载入的方法也不一样,具体以下所示:
JSON.parse()
解析返回结果。在编译过程当中,Node对获取的JS文件内容进行了头尾包装,这样,每一个文件模块之间都进行了做用域隔离。以下:
(function(exports, require, module, __filename, __dirname){
})
复制代码
模拟
require
方法的原理,以下:
// b.js
console.log('b.js')
exports.name = "b"
// a.js
let fs = require('fs');
let path = require('path')
let b = require2('./b.js')
function require2(mod) {
let filename = path.join(__dirname, mod);
let content = fs.readFileSync(filename, 'utf8');
let fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content + "\n return module.exports")
let module = {
exports: {}
}
return fn(module.exports, require2, module, __filename, __dirname)
}
// b.js
复制代码
exports
VS module.exports
经过exports
和module.exports
对外公开的方法均可以访问,但有区别。
exports
仅仅是 module.exports
的一个地址引用。
nodejs 只会导出 module.exports
的指向,若是 exports
指向变了,那就仅仅是 exports 不在指向 module.exports
,因而不会再被导出。
举个栗子,以下:
// test3.js
let counter = 0;
exports.printNextCount = function () {
counter += 2;
console.log(counter);
}
module.exports = function () {
counter += 10;
this.printNextCount = function () {
console.log(counter)
}
}
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
/* { printNextCount: [Function] } [Function] false */
// test3_require.js
let Counter = require('./test3.js')
let counterObj = new Counter();
counterObj.printNextCount();
/* 10 */
复制代码
举个栗子,入下:
// test1.js
let counter = 0;
exports.temp = function () {
counter += 10;
this.printNextCount = function () {
console.log(counter);
}
}
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
/* { temp: [Function] } // 是一个函数能够直接调用 { temp: [Function] } // 是一个函数能够直接调用 true */
// test1_require.js
// 只能做为函数调用
let counter = require('./test1')
console.log(counter) // { temp: [Function] }
counter.temp() // 只能做为函数调用
复制代码
使用这样的好处是exports只能对外暴露单个函数,可是module.exports却能暴露一个类
举个栗子,以下:
// test2.js
let counter = 0;
module.exports = function () {
counter += 10;
this.printNextCount = function () {
console.log(counter);
}
}
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
/* {} [Function] // 是一个类,须要new才能调用 false */
// test2_require.js
let Counter = require('./test2');
// 直接调用报错
// console.log(Counter.printNextCount()) // TypeError: Counter.printNextCount is not a function
// new一个对象再调用
let counterObj = new Counter();
counterObj.printNextCount();
/* 10 */
复制代码
module.exports
和exports
module.exports
,导出多个方法和变量用exports