JS 是世界上使用频率最高的语言之一,JS 的核心是在 Netscape 公司大行其道的岁月里创建的,当时正处于浏览器白热化时期,JS 作为打击 Microsoft 的利器匆忙上马。它并没有发展得十分成熟就被发布,这意味着不可避免地带有一些糟糕的特性。虽然开发时间不长,但 JS 被赋予一些强大的特性。不过“脚本间共享全局命名空间”这一特性却不在其中。
一旦 JS 代码被载入页面,就会被添加进全局命名空间,全局命名空间是被所有已载入的脚本所共享的通用地址空间,这样会导致安全性问题、命名冲突以及一些难以跟踪和解决的一般性错误。
值得庆幸的是 Node 作为服务端的 JS 制定了一些规范,并实现了 CommonJS 模块标准。在这个标准中,每个模块都拥有一个上下文,将该模块和其他模块隔离开来,这意味着模块不会污染全局作用域,因为根本就没有全局作用域,并且也不会对其他模块造成干扰。
Node是如何加载模块的?
在 Node 可用文件路径也可用名称来引用模块,除非是核心模块,否则用名称引用的模块最终都会被映射为一个文件路径。Node 核心模块将一些核心函数暴露给程序员,它们在 Node 进程启动时会被预先载入。其他模块包括使用 NPM 安装的第三方模块,以及用户自己创建的本地模块。
不管什么类型的模块,在被导入当前脚本后,程序员都可以使用其对外暴露一组公共 API 。不管使用什么模块,你都可以使用 require()
载入它。
// 导入一个核心模块或由NPM安装的模块
// require() 会返回一个对象,该对象表示模块对外暴露的JS API。
// 根据模块不同,该对象可能是任意的JS值,可是一个函数,也可是一个具有若干属性的对象,属性可能是函数、数组或其它任何类型的JS对象。
var module = require('module_name');
导出模块
Node 中 CommonJS 模块系统是文件之间共享对象或函数的唯一方式,对于足够复杂的应用,应将一些类、对象、函数划分成定义良好的可重用模块,模块值对外暴露你指定的内容。
Node 中文件和模块是一一对应的,Node 可导出复杂的对象,module.exports
被初始化成一个空对象,可为空对象附加上任意想要导出的属性。
$ vim circle.js
function Circle(r){
function square(){
return Math.pow(r,2);
}
function area(){
return Math.PI*square();
}
return {area:area};
}
// 定义模块导出的内容
// module是个变量表示当前模块自身
// module.exports 表示模块向需要它的脚本所导出的对象,可是任意对象。
// 导出Circle类的构造函数,用户可使用该函数来创建具备完整功能的Circle实例。
module.exports = Circle;
可设计模块让其导出一组函数或常量
$ vim module.js
function printA(){
console.log('a');
}
function printB(){
console.log('b');
}
function printC(){
console.log('c');
}
module.exports.printA = printA;
module.exports.printB = printB;
module.exports.printC = printC;
module.expots.pi = Math.PI;
使用模块的客户端脚本
$ vim test.js
var module = require('./module');
module.printA();
module.printB();
console.log(module.pi);
加载模块
可使用 require()
加载模块,在代码中调用 require()
不会改变全局命名空间的状态,因为在 Node 中根本就没有全局命名空间这个概念。若模块存在并没有任何语法或初始化错误,调用 require()
就会返回这个模块对象,然后就可以将这个对象赋值给任意一个局部变量。
引用模块的方式会决定模块的类型
- 核心模块
- NPM 安装的第三方模块
- 本地模块
加载核心模块
核心模块是指 NPM 中以二进制形式发布的模块,核心模块只能通过模块名引用,而不能通过文件路径引用。即使已经存在一个与其同名的第三方模块,也会优先加载核心模块。
// 加载http核心模块,返回http模块对象,该对象实现由Node API文件描述的 HTTP API.
var http = require('http');
加载文件模块
可通过提供绝对路径或相对路径从文件系统中加载非核心模块,可省略文件扩展名 .js
。若没找到此文件,Node会在文件名上添加 .js
扩展名再次查找路径。
// 以绝对路径从文件系统中加载非核心模块
var myModule = require('/home/user/modules/my_module');
// 基于当前文件的相对路径
var myModule = require('./lib/my_module');
var myModule = require('../modules/my_module');
// 模块扩展名可省略
var myModule = require('./my_module.js');
加载文件夹模块
// 可使用文件夹路径加载模块,Node会在指定文件夹下查找模块。
// Node假定该文件夹是一个包,并试图查找包定义。包定义包含在名为 package.json 文件中。
// 若文件夹中不存在包定义文件 package.json, 那么包入口点会假定为默认值 index.js。
// 若文件夹中存在 package.json,那么Node会尝试解析该文件并查找 main 属性,将 main 属性作为入口点的相对路径。
var myModule = require('./modules');// Node 将会在路径 ./modules/index.js 下查找文件
从 node_modules 文件夹加载
若一个模块名即不是相对路径也不是核心模块,Node 会尝试在当前目录下的 node_modules 文件夹中查找该模块。
// Node 会尝试查找文件 ./node_modules/module.js
// 若没有找到该文件,Node会继续在父文件夹 ../node_modules/module.js 中查找
// 若没有找到该文件,Node会继续查找上级父文件夹,这个过程一直持续到达根目录或找到所需的模块为止。
var myModule = require('module.js');
可利用此特性管理 node_modules 目录中的内容,不过最好还是让 NPM 来管理模块。本地模块 node_modules 是 NPM 安装模块的默认位置,此项功能将Node和NPM关联到一起。
缓存模块
模块在首次加载时会被缓存起来,这意味着若模块名能被解析为相同的文件名,那么每次调用 require('module') 都会确切地返回同一模块。
$ vim my_module.js
console.log('module my_module initializing...');
// 模块初始化过程只执行了一次,当构建自定义模块时,若模块在初始化时可能会产生副作用。
module.exports = function(){
console.log('hi');
};
console.log('my_module initialized')
模块初始化过程只执行了一次
var m1 = require('./my_module.js');
var m2 = require('./my_module');
exports 和 module.exports有什么区别呢?
首先需明确的是,module.exports
才是真正的接口,exports
只不过是module.exports
的一个辅助工具,exports
是基于module.exports
而实现的。实际上,全部由exports
获取的属性和方法,最后都赋给了module.exports
接口,不过有个前提是module.exports
本身不具备任何属性和方法。换言之,若module.exports
接口已具备属性和方法,exports
获取的属性和方法将被忽略。
总结
Node取消了JS默认的全局命名空间,而用CommonJS模块系统取而代之,这样可更好地组织代码,也因此避免了了一些安全问题和错误。
使用 require() 从文件或文件夹加载核心模块、第三方模块、自定义模块。可使用相对或绝对路径加载非核心模块。若将模块放入 node_modules 文件夹或使用 NPM 安装模块,也可使用模块名加载。
可编写JS文件导出标识模块API的对象,以此创建自定义模块。