VI-1 Node.js 模块
在编写稍大些的项目的时候,模块化和组件化是当前 JS 的最佳解决方案。在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文件就是一个模块,而文件路径就是模块名。
模块化代码在 nodejs 中有以下特性:
- Node.js是通过模块的形式进行组织与调用的,在编写每个模块时,都有
require
、exports
、module
三个预先定义好的变量可供使用。 - 所以系统自带了很多模块
- 同时也提供了新模块的扩展机制
1.1 require
require函数用于在当前模块中加载和使用别的模块,传入一个模块名
(路径),反回一个模块导出对象。模块名可使用相对路径(以./开头),或者是绝对路径(以/或C:之类的盘符开头)。另外,模块名中的.js扩展名可以省略。以下是一个例子。
var foo1 = require('./foo');
var foo2 = require('./foo.js');
var foo3 = require('/home/user/foo');
var foo4 = require('/home/user/foo.js');
// foo1至foo4中保存的是同一个模块的导出对象。
复制代码
另外,可以使用以下方式加载和使用一个JSON文件。
var data = require('./data.json');
复制代码
1.2 exports
exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。以下例子中导出了一个公有方法。
exports.hello = function () {
console.log('Hello World!');
};
复制代码
1.3 module
通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式。
module.exports = function () {
console.log('Hello World!');
};
复制代码
- 以上代码中,模块默认导出对象被替换为一个函数。
- 通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序的其它模块完成工作。
※ 官方实现require
的方式:
function require(...) {
var module = { exports: {} };
((module, exports) => {
// Your module code here. In this example, define a function.
function some_func() {};
exports = some_func;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
module.exports = some_func;
// At this point, the module will now export some_func, instead of the
// default object.
})(module, module.exports);
return module.exports;
}
复制代码
如果 a.js require 了 b.js, 那么在 b 中定义全局变量 t = 111 能否在 a 中直接打印出来?
① 每个 .js 能独立一个环境只是因为 node 帮你在外层包了一圈自执行, 所以你使用 t = 111 定义全局变量在其他地方当然能拿到. 情况如下:
// b.js
(function (exports, require, module, __filename, __dirname) {
t = 111;
})();
// a.js
(function (exports, require, module, __filename, __dirname) {
// ...
console.log(t); // 111
})();
复制代码
a.js 和 b.js 两个文件互相 require 是否会死循环? 双方是否能导出变量? 如何从设计上避免这种问题?
② 不会, 先执行的导出空对象, 通过导出工厂函数让对方从函数去拿比较好避免. 模块在导出的只是 var module = { exports: {} };
中的 exports, 以从 a.js 启动为例, a.js 还没执行完 exports 就是 {}
在 b.js 的开头拿到的就是 {}
而已.
另外还有非常基础和常见的问题, 比如 module.exports 和 exports 的区别这里也能一并解决了 exports 只是 module.exports 的一个引用. 没看懂可以在细看我以前发的帖子.
再晋级一点, 众所周知, node 的模块机制是基于 CommonJS 规范的. 对于从前端转 node 的同学, 如果面试官想问的难一点会考验关于 CommonJS 的一些问题. 比如比较 AMD, CMD, CommonJS 三者的区别, 包括询问关于 node 中 require 的实现原理等.
1.4 module.exports 与 exports
- 默认
exports
与module.exports
指向相同的对象的引用,并且此对象是一个空对象{}
。 - 对他们添加属性,不会破坏他们的一致性
console.log(module.exports === exports); // true
console.log(exports.a); // undefined
// 修改exports
module.exports.a = 100;
console.log(module.exports === exports); // true
console.log(exports); // { a: 100 }
// 修改exports
exports.b = {
a: 100
};
console.log(module.exports === exports); // true
console.log(exports); // { a: 100, b: { a: 100 } }
复制代码
- 对他们直接使用赋值号,则会破坏他们的引用关系
console.log(module.exports === exports); // true
module.exports = {c:100};
console.log(exports); // {}
console.log(module.exports); // {c:100}
console.log(module.exports === exports); // false
// 直接修改exports
console.log(module.exports === exports); // false
exports = {
c:100
};
console.log(exports); // {c:100}
console.log(module.exports); // {}
console.log(module.exports === exports); // false
复制代码
- 导出以module.exports为准
1.4 系统自带模块
可以通过
process.moduleLoadList
打印的 NativeModule 可以查看到相关的模块信息。主要系统包括:
在 V8.9.3中,主要的系统模块包括:
[ 'assert', 'buffer', 'console', 'dns', 'domain', 'events', 'fs', 'module', 'net', 'os', 'path', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tty', 'url', 'util', 'vm' ]
其中最能代表node.js的最初思想的是net, 'events'这两个模块。
1.5 系统 API
|稳定性:2 -- 稳定的
|v8.1.1
|
1.5.1 exports 和 module.exports
先看一个例子:
// module circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
复制代码
-
circle.js
文件导出了area()
和circumference()
两个函数。 - 通过在特殊的 exports 对象上指定额外的属性,函数和对象可以被添加到模块的根部。
- 模块内的本地变量是私有的,因为模块被 Node.js 包装在一个函数中。 (
PI
是私有的)
第二个例子:
// square.js
module.exports = class Square {
constructor(width) {
this.width = width;
}
area() {
return this.width ** 2;
}
};
// use square module
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`mySquare 的面积是 ${mySquare.area()}`);
复制代码
-
module.exports
属性可以被赋予一个新的值(例如函数或对象)。
1.5.2 主模块
-
当 Node.js 直接运行一个文件时,
require.main
会被设为它的 module。 这意味着可以通过 require.main === module 来判断一个文件是否被直接运行: -
module
提供了一个 filename 属性(通常等同于 __filename),所以可以通过检查 require.main.filename 来获取当前应用程序的入口点。
1.5.3 包管理器的技巧 http://nodejs.cn/api/modules.html#modules_addenda_package_manager_tips
1.5.4 缓存
-
模块在第一次加载后会被缓存。 这也意味着(类似其他缓存机制)如果每次调用 require('foo') 都解析到同一文件,则返回相同的对象。
-
多次调用 require(foo) 不会导致模块的代码被执行多次。 这是一个重要的特性。 借助它, 可以返回“部分完成”的对象,从而允许加载依赖的依赖, 即使它们会导致循环依赖。
-
如果想要多次执行一个模块,可以导出一个函数,然后调用该函数。
模块缓存的注意事项:
-
模块是基于其解析的文件名进行缓存的。 由于调用模块的位置的不同,模块可能被解析成不同的文件名(比如从 node_modules 目录加载),这样就不能保证 require('foo') 总能返回完全相同的对象。
-
此外,在不区分大小写的文件系统或操作系统中,被解析成不同的文件名可以指向同一文件,但缓存仍然会将它们视为不同的模块,并多次重新加载。 例如,require('./foo') 和 require('./FOO') 返回两个不同的对象,而不会管 ./foo 和 ./FOO 是否是相同的文件。
-
建议: 文件名称按照一定规范进行编排,无大小写转换后相同文件名。
1.5.6 核心模块
- 核心模块是 Nodejs将其编译成二进制的模块,便于更快速加载
- 核心模块存放在 Node.js 源代码的 lib/ 目录下。
- require() 总是会优先加载核心模块。 例如,require('http') 始终返回内置的 HTTP 模块,即使有同名文件。
1.5.7 循环
在模块之间的互相加载时,当 main.js
加载 a.js
时,a.js
又加载 b.js
。 此时,b.js
会尝试去加载 a.js
, 这样就造成了无限循环。
会返 a.js
的 exports
对象的未完成的副本给 b.js
模块。 然后 b.js
完成加载,并将 exports
对象提供给 a.js
模块。
DEMO:
//a.js:
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');
//b.js:
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');
//main.js:
console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
复制代码
分析过程:
- 关键词: 会返 a.js 的 exports 对象的未完成的副本 给 b.js 。
- 未完成程度 : 在出现循环引用的时候 返回副本。
- 目的: 不阻碍主流程。
- 执行顺序:1 -> 2 -> 5~7 -> 8~16 -> 3 -> 4
- 1~2 : 主流程
- 5~7:按照
require(a.js)
顺序执行, - 8~9:按照 b.js 数序执行,
- 10: 出现循环:制造 当前状态下 a.js 副本,状态保留至副本创造时。
- 11~16: 顺序执行,然后
b.js
完成加载,并将exports
对象提供给a.js
模块。 - 3: 缓存读取 b.js 但已经在 a.js 中执行过,这里不执行。
- 4: main.js 执行完毕。
- 输出结果:
$ node main.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true
复制代码
- 需要仔细的规划, 以允许循环模块依赖在应用程序内正常工作。
1.5.8 文件模块(系统自带的模块)
-
如果按确切的文件名没有找到模块,则 Node.js 会尝试带上
.js
、.json
或.node
拓展名再加载。-
.js
文件会被解析为 JavaScript 文本文件 -
.json
文件会被解析为 JSON 文本文件。 -
.node
文件会被解析为通过 dlopen 加载的编译后的插件模块。
-
-
以 '/' 为前缀的模块是文件的绝对路径。 例如,
require('/home/marco/foo.js')
会加载 /home/marco/foo.js 文件。 -
以 './' 为前缀的模块是相对于调用 require() 的文件的。
-
当没有以
'/'
、'./'
或'../'
开头来表示文件时,这个模块必须是一个核心模块或加载自node_modules
目录。 -
如果给定的路径不存在,则 require() 会抛出一个 code 属性为
'MODULE_NOT_FOUND'
的 Error。
1.5.9 目录作为模块
如果把程序和依赖库放在统一个文件夹下,提供一个单一的入口指向它。把目录传给 require()
作为一个参数,即为 目录作为模块 引用。
- 使用
package.json
指定main
入口模块:
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
复制代码
如果这是在 ./some-library 目录中,则 require('./some-library')
会试图加载 ./some-library/lib/some-library.js
。
- 无
package.json
指定,则 Nodejs 会试图加载index.js
或index.node
-
require('./some-library')
:./some-library/index.js
或./some-library/index.node
-
1.5.10 node_modules 目录加载
传入require()
的路径不是一个核心模块,Nodejs 从父目录开始,尝试从父目录的node_modules
中加载模块。
如果在'/home/ry/projects/foo.js'
文件里调用了 require('bar.js')
,则 Node.js 会按以下顺序查找:
- /home/ry/projects/node_modules/bar.js
- /home/ry/node_modules/bar.js
- /home/node_modules/bar.js
- /node_modules/bar.js
通过在模块名后包含一个路径后缀,可以请求特定的文件或分布式的子模块。 例如,require('example-module/path/to/file') 会把 path/to/file 解析成相对于 example-module 的位置。 后缀路径同样遵循模块的解析语法。 1.5.11 从全局目录加载 如果 NODE_PATH 环境变量被设为一个以冒号分割的绝对路径列表,则当在其他地方找不到模块时 Node.js 会搜索这些路径。
**注意:**在 Windows 系统中,NODE_PATH
是以分号间隔的。
在当前的模块解析算法运行之前,NODE_PATH
最初是创建来支持从不同路径加载模块的。
虽然 NODE_PATH 仍然被支持,但现在不太需要,因为 Node.js 生态系统已制定了一套存放依赖模块的约定。 有时当人们没意识到 NODE_PATH 必须被设置时,依赖 NODE_PATH 的部署会出现意料之外的行为。 有时一个模块的依赖会改变,导致在搜索 NODE_PATH 时加载了不同的版本(甚至不同的模块)。
此外,Node.js 还会搜索以下位置:
- $HOME/.node_modules
- $HOME/.node_libraries
- $PREFIX/lib/node
其中 $HOME
是用户的主目录,$PREFIX
是 Node.js 里配置的 node_prefix
。
这些主要是历史原因。
注意:强烈建议将所有的依赖放在本地的 node_modules 目录。 这样将会更快地加载,且更可靠。
1.5.12 [Module Scope]
__dirname
-
<string>
当前模块的文件夹的名字 - 等同于:
path.dirname(__filename)
的值
DEMO: 运行 /Users/demo/example.js
console.log(__dirname);
// Prints: /Users/demo
console.log(path.dirname(__filename));
// Prints: /Users/demo
复制代码
1.5.12 [Module Scope]
__filename
-
<string>
当前模块的文件名称---解析后的绝对路径。
DEMO: 运行/Users/demo/example.js
console.log(__filename);
// Prints: /Users/demo/example.js
console.log(__dirname);
// Prints: /Users/demo
复制代码
给定两个模块: a 和 b, 其中 b 是 a 的一个依赖。
文件目录结构如下:
- /Users/mjr/app/a.js
- /Users/mjr/app/node_modules/b/b.js
使用__filename
===>
- b.js 中对 __filename 的引用将会返回
/Users/mjr/app/node_modules/b/b.js
- a.js 中对 __filename 的引用将会返回
/Users/mjr/app/a.js
1.5.13 [Module Scope]
exports / module
- exports: 这是一个对于 module.exports 的更简短的引用形式。
- module:对当前模块的引用,
1.5.14 require()
用于引入模块
-
require.cache
: 被引入的模块将被缓存在这个对象中。从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。注意不能删除 native addons(原生插件),因为它们的重载将会导致错误。 -
require.resolve(request[, options])
使用内部的 require() 机制查询模块的位置, 此操作只返回解析后的文件名,不会加载该模块。- request
<string>
需要解析的模块路径。 - options
<Object>
- paths
<Array>
解析模块的起点路径。此参数存在时,将使用这些路径而非默认解析路径。 注意此数组中的每一个路径都被用作模块解析算法的起点,意味着 node_modules 层级将从这里开始查询。
- paths
- Returns:
<string>
- request
-
require.resolve.paths(request])
返回一个数组,其中包含解析 request 过程中被查询的路径。 如果 request 字符串指向核心模块(例如 http 或 fs),则返回 null。- request:
<string>
被查询解析路径的模块的路径。 - 返回:
<Array> | <null>
- request:
DEMO:
// modules
> require.resolve.paths('aaa')
[ '/Users/zhengao/repl/node_modules',
'/Users/zhengao/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/zhengao/.node_modules',
'/Users/zhengao/.node_libraries',
'/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node',
'/Users/zhengao/.node_modules',
'/Users/zhengao/.node_libraries',
'/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node' ]
// 核心模块
> require.resolve.paths('http')
null
复制代码
1.5.15 module对象
<Object>
- 在每个模块中,module 的*变量是一个指向表示当前模块的对象的引用。 为了方便,module.exports 也可以通过全局模块的 exports 对象访问。
- module 不是全局的,而是每个模块本地的。只不过每个模块都有一个 module 对象而已。
> module
Module {
id: '<repl>',
exports: {},
parent: undefined,
filename: null,
loaded: false,
children: [],
paths: [
'/Users/zhengao/repl/node_modules',
'/Users/zhengao/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/zhengao/.node_modules',
'/Users/zhengao/.node_libraries',
'/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node'
]
}
复制代码
-
module.children
:<Array>
- 被该模块引用的模块对象。
-
module.exports
:<Object>
- module.exports 对象是由模块系统创建的。许多人希望他们的模块成为某个类的实例。 为了实现这个,需要将期望导出的对象赋值给 module.exports。
- **注意,**将期望的对象赋值给 exports 会简单地重新绑定本地 exports 变量,这可能不是期望的。
DEMO:
// 许多人希望他们的模块成为某个类的实例, 需要将期望导出的对象赋值给 module.exports
//a.js
const EventEmitter = require('events');
module.exports = new EventEmitter();
// 处理一些工作,并在一段时间后从模块自身触发 'ready' 事件。
setTimeout(() => {
module.exports.emit('ready');
}, 1000);
// 然后,在另一个文件中可以这么做:
// b.js
const a = require('./a.js');
a.on('ready', () => {
console.log('模块 a 已准备好');
});
复制代码
*DEMO: *
// 注意,对 module.exports 的赋值必须立即完成。 不能在任何回调中完成。否则无效
// x.js:
setTimeout(() => {
module.exports = { a: 'hello' };
}, 0);
// y.js:
const x = require('./x');
console.log(x.a);
复制代码
-
exports
快捷方式- exports 变量是在模块的文件级别作用域内有效的,它在模块被执行前被赋予 module.exports 的值。
- 它有一个快捷方式,以便
module.exports.f = ...
可以被更简洁地写成exports.f = ...
。 - 注意,就像任何变量,如果一个新的值被赋值给 exports,它就不再绑定到 module.exports:
module.exports.hello = true; // 从对模块的引用中导出
exports = { hello: false }; // 不导出,只在模块内有效
//当 module.exports 属性被一个新的对象完全替代时,也会重新赋值 exports,例如:
module.exports = exports = function Constructor() {
// ... 及其他
};
//为了解释这个行为,想象对 require() 的假设实现,它跟 require() 的实际实现相当类似:
function require(/* ... */) {
const module = { exports: {} };
((module, exports) => {
// 模块代码在这。在这个例子中,定义了一个函数。
function someFunc() {}
exports = someFunc;
// 此时,exports 不再是一个 module.exports 的快捷方式,
// 且这个模块依然导出一个空的默认对象。
module.exports = someFunc;
// 此时,该模块导出 someFunc,而不是默认对象。
})(module, module.exports);
return module.exports;
}`
复制代码
-
module.filename
: 模块的完全解析后的文件名。 -
module.id
:<string>
模块的标识符。 通常是完全解析后的文件名。 -
module.loaded
:<boolean>
模块是否已经加载完成,或正在加载中。 -
module.parent
:<Object 模块对象>
最先引用该模块的模块。 -
module.paths
:<String []>
模块的搜索路径。 -
module.require(id)
:-
id
:<string>
- 返回:
<Object>
已解析的模块的module.exports
- module.require 方法提供了一种类似 require() 从原始模块被调用的加载模块的方式。
-
-
module.builtinModules
:由Node.js提供的所有模块的名称列表。可以用来验证模块是否被第三方模块维护。
1.6 Q&A
Q1: 比较AMD,CMD和 CommonJS 三者区别 A:
背景: 网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑。因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
- CommonJS:
CommonJS规范是诞生比较早的。NodeJS就采用了CommonJS。CommonJS 是一种同步的模块化规范,是这样加载模块:
var clock = require('clock');
clock.start();
复制代码
这种写法适合服务端,因为在服务器读取模块都是在本地磁盘,加载速度很快。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。
这就是AMD规范诞生的背景。比如上面的例子中clock的调用必须等待clock.js
请求成功,加载完毕。
- AMD:
AMD,即 (Asynchronous Module Definition),这种规范是异步的加载模块,requireJs应用了这一规范。先定义所有依赖,然后在加载完成后的回调函数中执行:
require([module], callback);
第一个参数[module]
,是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
复制代码
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js
和 curl.js
。
AMD虽然实现了异步加载,但是开始就把所有依赖写出来是不符合书写的逻辑顺序的,能不能像commonJS
那样用的时候再require,而且还支持异步加载后再执行呢?
- CMD:
CMD (Common Module Definition), 是seajs
推崇的规范,CMD则是依赖就近,用的时候再require。它写起来是这样的:
define(function(require, exports, module) {
var clock = require('clock');
clock.start();
});
复制代码
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;
而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
-
总结:
- CommonJS: Nodejs 使用的规范,同步加载。适合后端
- AMD,CMD:异步加载,require 时机不同,AMD:头部引用, CMD 随时引用
- 参考:
Q2:Node.js 中 require()
的实现
A:
Q3: 什么时候使用 exports ,什么时候使用 module.exports
A:用一句话来说明就是,
require
方能看到的只有module.exports
这个对象,它是看不到exports
对象的,而我们在编写模块时用到的exports
对象实际上只是对module.exports
的引用。
var module = {
exports:{
name:"我是module的exports属性"
}
};
//exports是对module.exports的引用,也就是exports现在指向的内存地址和module.exports指向的内存地址是一样的
var exports = module.exports;
console.log(module.exports); // { name: '我是module的exports属性' }
console.log(exports); // { name: '我是module的exports属性' }
exports.name = "我想改一下名字";
console.log(module.exports); // { name: '我想改一下名字' }
console.log(exports); // { name: '我想改一下名字' }
//看到没,引用的结果就是a和b都操作同一内存地址下的数据
//这个时候我在某个文件定义了一个想导出的模块
var Circle = {
name:"我是一个圆",
func:function(x){
return x*x*3.14;
}
};
exports = Circle; // 看清楚了,Circle这个Object在内存中指向了新的地址,所以exports也指向了这个新的地址,和原来的地址没有半毛钱关系了
console.log(module.exports); // { name: '我想改一下名字' }
console.log(exports); // { name: '我是一个圆', func: [Function] }
复制代码
- 直接更改引用:
exports
和module.exports
指向的是同一个引用 - 直接赋值: 赋值给
exports
内部作用域使用,赋值给module.exports
可以被require()
引用。
1.7 模块化的基本要求
- 高内聚
- 低耦合
- 逻辑清晰正确
- 要有输出
- 高扇入,低扇出
- 减少冗余(相同类型不要超过三遍)
1.8 模块化的代码规范
在 Node.js 中使用 CommonJS 使用模块规范