CommonJs可以做以下内容:
1、服务端应用程序
2、命令行工具
3、基于桌面的应用程序
4、混合开发
1. CommonJs规范
关于模块:
- 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
- 在模块中使用global 定义全局变量,不需要导出,在别的文件中可以访问到。
- 每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。
- 通过 require加载模块,读取并执行一个js文件,然后返回该模块的exports对象。
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
2. module对象
node内部提供一个Module构建函数。所有的模块其实都是Module的实例。
每一个模块内部,都有一个Module对象,代表当前模块。它有以下属性:
module.id
,模块的识别符,通常是带有绝对路径的模块文件名;module.filename
,模块的文件名,带有绝对路径;module.loaded
,返回一个boolean值,表示模块是否已经完成加载;module.parent
,返回一个对象,表示调用该模块的模块;module.children
,返回一个数组,表示该模块内用到的其他模块;module.exports
,表示模块对外输出的值;
下图中打印module的结果:
2.1 module.exports属性和exports变量
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
为了方便,node为每一个模块提供了一个exports变量,指向module.exports。这等同在每个模块头部,有一条这样的命令。
var exports = module.exports;
所以我们在对外输出模块接口时,可以向exports对象中添加方法或者属性。下面这么写都是可以的。
module.exports.name = "zhangsan";
exports.name = 'lisi'
exports.age = 18;
但是注意,不可以直接将exports变量指向一个值
,因为这样等于切断了exports和module.exports的联系。这意味着,如果一个模块的对外接口,是一个单一的值,这种情况就不能使用exports输出,只能使用module.exports输出。
exports = "lisi"; //错误的
exports = function(){return "lisi"}; //错误的
下面这种写法中,hello函数是无法对外输出的,因为module.exports被重新赋值了
。
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
如果你觉得exports 和 module.exports 之间的区别很难分清,一个简单的处理方法,就是放弃使用exports ,只使用 module.exports。
3. require
require命令
用于加载模块文件
。它的基本功能是,读取并执行一个Javascript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
3.1 require的内部处理流程
当使用require加载模块时,在node内部,会做如下处理:
- 先检查Moducle._cache, 看里面有没有对应模块的缓存;
- 如果缓存没有的话,就创建一个新的module实例,并把它添加到缓存中,缓存的是它exports导出的值;
- 如果缓存中有的话,使用module.load()这个方法,去加载这个模块,读取文件内容后,使用module.compile()执行文件代码;
- 如果解析的过程中,出现异常,就从缓存中删除这个模块;
- 如果没有出现异常,最后返回这个模块的module.exports;
require命令用于加载文件,后缀名默认为.js
。
var user = require('./user');
//等同于
var user = require('./user.js');
3.1 加载规则
根据参数的不同格式,require命令会去不同路径寻找模块文件。
- 如果参数字符串以
“/”
开头,表示加载的是一个位于绝对路径
的模块文件。 - 如果参数字符串以
“./”
开头,表示加载的是一个位于相对路径
(跟当前执行脚本的位置相比)的模块文件。 - 如果参数字符串
不以“/”或者“./”开头
,则表示加载的是一个默认提供的核心模块
(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块
(全局安装或局部安装)。 - 如果参数字符串不以“/”或者“./”开头,而是一个路径,则会先找到该路径目录,然后再以它为参数找到后续路径。
- 如果指定的模块文件没有被发现,Node会尝试为文件添加.js、.json、.node后,再去搜索。.js文件会以文本格式的javascript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
- 如果想得到require命令加载的确切文件名,使用require.resolve()方法。
3.2 模块的缓存
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。
3.2.1 模块缓存的案例1
require('./user.js');
require('./user.js').message = "hello";
require('./user.js').message
// "hello"
上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。
3.2.1 模块缓存的案例2
再来看下面这个案例:
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
上面的例子中,当执行main.js时,加载了a.js,先输出x为a1,又加载了b.js,b.js中输出x为b1,又加载a.js,此时因为之前已经加载过a.js了,所以直接从缓存中读取,a.js的x为a1,所以先打印出"b.js a1";然后b.js输出x为b2,所以a.js中打印“a.js b2",然后a.js中又输出x为a2;所以main.js中打印”main.js a2",第二次再打印的时候,因为a.js和b.js都已经被缓存了,所以直接读取它们的exports的值,所以直接打印出“main.js b2"。
所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
注意:缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。
3.2.3 require.main
require方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。
直接执行的时候(node module.js),require.main属性指向模块本身。
require.main === module
// true
调用执行的时候(通过require加载该脚本执行),上面的表达式返回false。
4. 深入了解模块原理
hello.js
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
Node.js加载了hello.js后,它可以把代码包装一下,变成这样执行:
(function () {
// 读取的hello.js代码:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
// hello.js代码结束
})();
这样一来,原来的全局变量s现在变成了匿名函数内部的局部变量。如果Node.js继续加载其他模块,这些模块中定义的“全局”变量s也互不干扰。
所以,Node利用JavaScript的函数式编程的特性,轻而易举地实现了模块的隔离。
但是,模块的输出module.exports怎么实现?
这个也很容易实现,Node可以先准备一个对象module:
// 准备module对象:
var module = {
id: 'hello',
exports: {}
};
var load = function (module) {
// 读取的hello.js代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js代码结束
return module.exports;
};
var exported = load(module);
var exports= module.exports;
//exports="hellworld"; 错误
// 保存module:
save(module, exported);
可见,变量module是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在hello.js中可以直接使用变量module原因就在于它实际上是函数的一个参数:
module.exports = greet;
通过把参数module传递给load()函数,hello.js就顺利地把一个变量传递给了Node执行环境,Node会把module变量保存到某个地方。
由于Node保存了所有导入的module,当我们用require()获取module时,Node找到对应的module,把这个module的exports变量返回,这样,另一个模块就顺利拿到了模块的输出。