🍂落页
登 录

Node.js 笔记

  • 模块:CommonJS
  • 模块:ES Modules
🍂落页
TALAXY
模块:CommonJS
🍂落页
TALAXY

模块:CommonJS

模块:CommonJS
  • 启用方式
  • 获取主模块
  • .mjs 扩展
  • 总览
  • 缓存
  • 核心模块
  • 循环引用
  • 文件模块
  • 文件夹作为模块
  • 从 node_modules 加载模块
  • Loading from the global folders
  • 模块包装器
  • The module scope
  • The module object
  • The Module object
  • Source map v3 support

原文地址 ,GitHub

CommonJS 是 Node 模块化 JS 代码的原始方式。

导入和导出的例子:

const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;

模块会被 Node 包裹在一个函数里,因此模块里的本地变量会是私有的。比如例子中的 PI 。

可以直接给 module.exports 赋新值,可以是个函数或者类,等等。

启用方式

默认情况下,以下情形 Node 会视为 CommonJS 模块:

  • 扩展名为 .cjs 的文件;
  • 扩展名为 .js 的文件,且离文件最近的 package.json 里指定了 "type": "commonjs" ;
  • 扩展名为 .js 或者没有扩展名的文件,且最近的 package.json 不存在 "type" 字段,或者没有 package.json 文件;或者当文件被作为 ES Modules 解析时出现了语法错误。包作者应当明确 "type" 字段,即便所有的代码都是 CommonJS 模块。
  • 扩展名不为 .mjs、.cjs、.json、.node、.js 的文件(当最近的 package.json 指定了 "type": "module" 时:如果文件是通过 require() 导入的,则会视为 CommonJS 模块;如果是作为了程序的主入口,反之)。

See Determining module system for more details.

调用 require() 会使用 CommonJS 模块加载器。而调用 import() 会使用 ES Modules 加载器。

获取主模块

当一个文件被直接用 Node 运行,require.main 会被设置为该文件的 module 。因此可以用 require.main === module 判断当前文件是否是被直接运行。

比如对于 foo.js ,如果通过 node foo.js 运行结果会是 true ;但如果通过 require('./foo') 引入,结果会是 false 。

如果入口文件不是 CommonJS 模块,require.main 将会是 undefined ,且没有别的办法获取主模块。

.mjs 扩展

因为 require() 是同步的,所以不能用于加载 ES Modules 。应当使用 import() 。

同样的,.mjs 文件也不能用 require() 导入,因为 .mjs 本身会被 Node 视为 ES Modules 。

总览

如果想知道 require() 具体引入哪个文件,可以用 require.resolve() 获取。

若在路径为 Y 的模块中调用 require(X) ,require() 的执行逻辑如下:

  1. 如果 X 是核心模块,则返回该核心模块
  2. 如果 X 以 '/' 开头,则以 Y 作为文件跟路径
  3. 如果 X 以 './' 或 '/' 或 '../' 开头,执行:
    1. LOAD_AS_FILE(Y + X)
    2. LOAD_AS_DIRECTORY(Y + X)
    3. 抛出错误 "not found"
  4. 如果 X 以 '#' 开头,执行:
    1. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
  5. 执行 LOAD_PACKAGE_SELF(X, dirname(Y))
  6. 执行 LOAD_NODE_MODULES(X, dirname(Y))
  7. 抛出错误 "not found"

更多见 All togeter 。

缓存

模块会在第一次加载后被缓存起来。在后续的同文件的 require() 调用会得到相同的模块对象。

在面对模块之间的循环引用时,Node 会先返回一个未完成的模块对象。

如果想要让一个模块里的代码重复执行几次,可以将其作为函数导出,通过调用函数来进行重复执行。

模块缓存的使用陷阱

模块缓存是基于当初被解析的具体文件名。然而一个模块名可能会因为调用位置不同,分别被解析到不同的文件,所以并不会 保证 require('foo') 永远返回相同的对象。

除此之外,Node 会区分模块名以及所解析的文件名的大小写,即便是在一些不区分文件大小写的操作系统上。

核心模块

Node 内置了一些模块,它们被编译为了二进制文件。这些核心模块放在了 lib/ 目录里。

核心模块可使用 node: 前缀作为标识,它会绕过 require 的缓存机制。比如 require('node:http') 会直接返回 HTTP 模块,而不是从 require.cache 获取模块引用。

当传入给 require() 的模块标识与一些内置核心模块重名时,会直接取用加载核心模块。比如对于 require('http') 永远会返回内置的 HTTP 模块,即便有这么一个同名的模块文件。这些内置的核心模块也可以通过 module.builtinModules 访问(前提是没有用 node: 前缀)。

循环引用

当碰到循环 require() 调用时,模块可能还未执行完毕就被返回了。考虑这种情形:

// `a.js`
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
// `b.js`
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
// `main.js`
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

例子中 main.js 加载了 a.js ,a.js 加载了 b.js 。与此同时 b.js 也尝试加载 a.js 。为了避免死循环,a.js 会先将一份 未完成 的导出对象的 拷贝 返回给 b.js 模块。b.js 因此结束加载,并且把 exports 返回给了 a.js 模块。最后的输出结果如下:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true

如果应用中需要循环引用模块,应当小心使用。

文件模块

如果未找到给定的文件名,Node 会依次尝试给文件名添加 .js、.json、.node 再次查找。如果需要导入一个 .cjs 文件,则需要 在 require() 里指定文件的完整名称,比如 require('./file.cjs') 。

  • .json 会被解析为 JSON 对象;

  • .node 会被视为 addon 模块,并用 process.dlopen() 加载;

  • 其他扩展名的文件会被解析为 JS 代码文件。

  • 如果模块名是以 '/' 作为前缀,则代表绝对路径。比如 require('/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。

  • 如果模块名是以 './' 作为前缀,则其路径会相对于引用该模块的文件的路径。比如在 foo.js 中如果要调用 require('./circle') ,则需要两文件在同一个目录中。

  • 如果模块名没有 '/'、'./'、'../' 前缀,则必须是个核心模块,或着是一个 node_modules 文件夹。

  • 如果根据模块名最终未找到对应的文件,require() 会抛出一个 MODULE_NOT_FOUND 错误。

文件夹作为模块

Stability: 3 - Legacy: Use subpath exports or subpath imports instead.

有三种方法可以将文件夹作为 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 未指定 "main" ,或者甚至没有 package.json ,则 Node 会尝试在文件夹中寻找 index.js 或 index.node 文件。如果仍未找到,Node 会报错:

Error: Cannot find module 'some-library'

在所有三种方法中,如果调用 import('./some-library') 都会导致一个 ERR_UNSUPPORTED_DIR_IMPORT 。使用 package 的 subpath exports 及 subpath imports 则可以提供相同的文件组织结构来将文件夹作为模块,并可用于 require 或 import 导入。

从 node_modules 加载模块

如果传给 require() 的模块名并不是核心模块,并且不以 '/'、'../'、'./' 开头,则 Node 会从当前模块所在的目录下寻找 /node_modules ,并从中寻找模块。如果未找到,则再向上级文件夹寻找。

模块名里面也可带上具体的路径,来表示引入模块中某个具体的文件。比如 require('example-module/path/to/file') 会在 example-module 中寻找路径为 path/to/file 的文件。

Loading from the global folders

If the NODE_PATH environment variable is set to a colon-delimited list of absolute paths, then Node.js will search those paths for modules if they are not found elsewhere.

On Windows, NODE_PATH is delimited by semicolons (;) instead of colons.

NODE_PATH was originally created to support loading modules from varying paths before the current module resolution algorithm was defined.

NODE_PATH is still supported, but is less necessary now that the Node.js ecosystem has settled on a convention for locating dependent modules. Sometimes deployments that rely on NODE_PATH show surprising behavior when people are unaware that NODE_PATH must be set. Sometimes a module's dependencies change, causing a different version (or even a different module) to be loaded as the NODE_PATH is searched.

Additionally, Node.js will search in the following list of GLOBAL_FOLDERS:

  • 1: $HOME/.node_modules
  • 2: $HOME/.node_libraries
  • 3: $PREFIX/lib/node

Where $HOME is the user's home directory, and $PREFIX is the Node.js configured node_prefix.

These are mostly for historic reasons.

It is strongly encouraged to place dependencies in the local node_modules folder. These will be loaded faster, and more reliably.

模块包装器

在执行一个模块的代码之前,Node 会将其包装在一个函数里:

(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});

这样做的目的有:

  • 能够保证模块里的顶层变量不暴露在全局对象上;
  • 提供一些专属于模块的类全局变量,比如:
    • 用于导出的 module 及 exports 对象;
    • 标识模块文件位置的变量,比如 __filename、__dirname 。

The module scope

__dirname

  • {string}

The directory name of the current module. This is the same as the path.dirname() of the __filename.

Example: running node example.js from /Users/mjr

console.log(__dirname);
// Prints: /Users/mjr
console.log(path.dirname(__filename));
// Prints: /Users/mjr

__filename

  • {string}

The file name of the current module. This is the current module file's absolute path with symlinks resolved.

For a main program this is not necessarily the same as the file name used in the command line.

See __dirname for the directory name of the current module.

Examples:

Running node example.js from /Users/mjr

console.log(__filename);
// Prints: /Users/mjr/example.js
console.log(__dirname);
// Prints: /Users/mjr

Given two modules: a and b, where b is a dependency of a and there is a directory structure of:

  • /Users/mjr/app/a.js
  • /Users/mjr/app/node_modules/b/b.js

References to __filename within b.js will return /Users/mjr/app/node_modules/b/b.js while references to __filename within a.js will return /Users/mjr/app/a.js.

exports

  • {Object}

A reference to the module.exports that is shorter to type. See the section about the exports shortcut for details on when to use exports and when to use module.exports.

module

  • {module}

A reference to the current module, see the section about the module object. In particular, module.exports is used for defining what a module exports and makes available through require().

require(id)

  • id {string} module name or path
  • Returns: {any} exported module content

Used to import modules, JSON, and local files. Modules can be imported from node_modules. Local modules and JSON files can be imported using a relative path (e.g. ./, ./foo, ./bar/baz, ../foo) that will be resolved against the directory named by __dirname (if defined) or the current working directory. The relative paths of POSIX style are resolved in an OS independent fashion, meaning that the examples above will work on Windows in the same way they would on Unix systems.

// Importing a local module with a path relative to the `__dirname` or current
// working directory. (On Windows, this would resolve to .\path\myLocalModule.)
const myLocalModule = require('./path/myLocalModule');

// Importing a JSON file:
const jsonData = require('./path/filename.json');

// Importing a module from node_modules or Node.js built-in module:
const crypto = require('node:crypto');

require.cache

  • {Object}

Modules are cached in this object when they are required. By deleting a key value from this object, the next require will reload the module. This does not apply to native addons, for which reloading will result in an error.

Adding or replacing entries is also possible. This cache is checked before built-in modules and if a name matching a built-in module is added to the cache, only node:-prefixed require calls are going to receive the built-in module. Use with care!

const assert = require('node:assert');
const realFs = require('node:fs');

const fakeFs = {};
require.cache.fs = { exports: fakeFs };

assert.strictEqual(require('fs'), fakeFs);
assert.strictEqual(require('node:fs'), realFs);

require.extensions

Stability: 0 - Deprecated

require.main

  • {module | undefined}

The Module object representing the entry script loaded when the Node.js process launched, or undefined if the entry point of the program is not a CommonJS module. See "Accessing the main module".

In entry.js script:

console.log(require.main);
node entry.js
Module {
  id: '.',
  path: '/absolute/path/to',
  exports: {},
  filename: '/absolute/path/to/entry.js',
  loaded: false,
  children: [],
  paths:
   [ '/absolute/path/to/node_modules',
     '/absolute/path/node_modules',
     '/absolute/node_modules',
     '/node_modules' ] }

require.resolve(request[, options])

  • request {string} The module path to resolve.
  • options {Object}
    • paths {string[]} Paths to resolve module location from. If present, these paths are used instead of the default resolution paths, with the exception of GLOBAL_FOLDERS like $HOME/.node_modules, which are always included. Each of these paths is used as a starting point for the module resolution algorithm, meaning that the node_modules hierarchy is checked from this location.
  • Returns: {string}

Use the internal require() machinery to look up the location of a module, but rather than loading the module, just return the resolved filename.

If the module can not be found, a MODULE_NOT_FOUND error is thrown.

require.resolve.paths(request)
  • request {string} The module path whose lookup paths are being retrieved.
  • Returns: {string[]|null}

Returns an array containing the paths searched during resolution of request or null if the request string references a core module, for example http or fs.

The module object

  • {Object}

In each module, the module free variable is a reference to the object representing the current module. For convenience, module.exports is also accessible via the exports module-global. module is not actually a global but rather local to each module.

module.children

  • {module[]}

The module objects required for the first time by this one.

module.exports

  • {Object}

The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Assigning the desired object to exports will simply rebind the local exports variable, which is probably not what is desired.

For example, suppose we were making a module called a.js:

const EventEmitter = require('node:events');

module.exports = new EventEmitter();

// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(() => {
  module.exports.emit('ready');
}, 1000);

Then in another file we could do:

const a = require('./a');
a.on('ready', () => {
  console.log('module "a" is ready');
});

Assignment to module.exports must be done immediately. It cannot be done in any callbacks. This does not work:

x.js:

setTimeout(() => {
  module.exports = { a: 'hello' };
}, 0);

y.js:

const x = require('./x');
console.log(x.a);

exports shortcut

The exports variable is available within a module's file-level scope, and is assigned the value of module.exports before the module is evaluated.

It allows a shortcut, so that module.exports.f = ... can be written more succinctly as exports.f = .... However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:

module.exports.hello = true; // Exported from require of module
exports = { hello: false };  // Not exported, only available in the module

When the module.exports property is being completely replaced by a new object, it is common to also reassign exports:

module.exports = exports = function Constructor() {
  // ... etc.
};

To illustrate the behavior, imagine this hypothetical implementation of require(), which is quite similar to what is actually done by require():

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // Module code here. In this example, define a function.
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
}

module.filename

  • {string}

The fully resolved filename of the module.

module.id

  • {string}

The identifier for the module. Typically this is the fully resolved filename.

module.isPreloading

  • Type: {boolean} true if the module is running during the Node.js preload phase.

module.loaded

  • {boolean}

Whether or not the module is done loading, or is in the process of loading.

module.parent

Stability: 0 - Deprecated: Please use require.main and module.children instead.

module.path

  • {string}

The directory name of the module. This is usually the same as the path.dirname() of the module.id.

module.paths

  • {string[]}

The search paths for the module.

module.require(id)

  • id {string}
  • Returns: {any} exported module content

The module.require() method provides a way to load a module as if require() was called from the original module.

In order to do this, it is necessary to get a reference to the module object. Since require() returns the module.exports, and the module is typically only available within a specific module's code, it must be explicitly exported in order to be used.

The Module object

This section was moved to Modules: module core module.

  • module.builtinModules
  • module.createRequire(filename)
  • module.syncBuiltinESMExports()

Source map v3 support

This section was moved to Modules: module core module.

  • module.findSourceMap(path)
  • Class: module.SourceMap
    • new SourceMap(payload)
    • sourceMap.payload
    • sourceMap.findEntry(lineNumber, columnNumber)

TALAXY 落于 2024年3月18日 。