欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[Node精选] Node.js 模块相关

程序员文章站 2022-07-12 09:31:48
...

VI-1 Node.js 模块

在编写稍大些的项目的时候,模块化和组件化是当前 JS 的最佳解决方案。在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文件就是一个模块,而文件路径就是模块名。

模块化代码在 nodejs 中有以下特性

  1. Node.js是通过模块的形式进行组织与调用的,在编写每个模块时,都有requireexportsmodule 三个预先定义好的变量可供使用。
  2. 所以系统自带了很多模块
  3. 同时也提供了新模块的扩展机制

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

  1. 默认exportsmodule.exports指向相同的对象的引用,并且此对象是一个空对象{}
  2. 对他们添加属性,不会破坏他们的一致性
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 } }
复制代码
  1. 对他们直接使用赋值号,则会破坏他们的引用关系
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
复制代码
  1. 导出以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, 这样就造成了无限循环。

为了防止无限的循环,Nodejs 提供了一个解决策略。

会返 a.jsexports 对象的未完成的副本给 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.jsindex.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 还会搜索以下位置:

  1. $HOME/.node_modules
  2. $HOME/.node_libraries
  3. $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 层级将从这里开始查询。
    • Returns: <string>
  • require.resolve.paths(request]) 返回一个数组,其中包含解析 request 过程中被查询的路径。 如果 request 字符串指向核心模块(例如 http 或 fs),则返回 null。

    • request: <string> 被查询解析路径的模块的路径。
    • 返回: <Array> | <null>

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.jscurl.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的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。


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] }

复制代码
  • 直接更改引用: exportsmodule.exports 指向的是同一个引用
  • 直接赋值: 赋值给exports 内部作用域使用,赋值给 module.exports 可以被require() 引用。

1.7 模块化的基本要求

  • 高内聚
  • 低耦合
  • 逻辑清晰正确
  • 要有输出
  • 高扇入,低扇出
  • 减少冗余(相同类型不要超过三遍)

1.8 模块化的代码规范

在 Node.js 中使用 CommonJS 使用模块规范