Home  >  Article  >  Web Front-end  >  Quickly understand the module system in Nodejs in one article

Quickly understand the module system in Nodejs in one article

青灯夜游
青灯夜游forward
2021-09-14 10:32:102470browse

This article will take you through the module system in Nodejs, I hope it will be helpful to you!

Quickly understand the module system in Nodejs in one article

Modular background

Early JavaScript was used to implement simple page interaction logic, but with the development of the times, browsers Not only can it present simple interactions, various websites have begun to shine. As websites begin to become more complex and front-end codes increase, compared to other static languages, JavaScript's lack of modularity begins to be exposed, such as naming conflicts. Therefore, in order to facilitate the maintenance and management of front-end code, the community began to define modular specifications. In this process, many modular specifications have emerged, such as CommonJS, AMD, CMD, ES modules. This article mainly Explain the modularization implemented in Node based on CommonJS.

CommonJS specification

First of all, in the Node world, the module system complies with the CommonJS specification. In the CommonJS specification The definition, simply put, is:

  • Each file is a module
  • The information of a module is represented by the module object
  • PassexportsUsed to export the information exposed by the module to the outside world
  • Reference a module through require

Node module classification

  • Core modules: Modules such as fs, http, path, etc. These modules do not need to be installed and are already loaded in the memory at runtime. [Recommended learning: "nodejs Tutorial"]
  • Third-party modules: stored in node_modules through installation.
  • Custom module: mainly refers to the file module, which is introduced through absolute path or relative path.

Module object

We said above that a file is a module, and the current module information is described through a module object. A module object corresponds to The following properties: - id: the id of the current module - path: the path corresponding to the current module - exports: variables exposed by the current module to the outside world - parent: is also a module object, indicating the parent module of the current module, that is, the module that calls the current module - filename: the file name of the current module (absolute path), which can be used to add the loaded module to the global module cache when the module is introduced. Subsequent introductions can directly obtain the value from the cache. - loaded: indicates whether the current module has been loaded - children: is an array that stores the modules called by the current module - paths: is an array that records the search for the node_modules directory starting from the current module, and recursively searching upwards to the node_modules directory under the root directory

module.exports and exports

After talking about CommonJS specifications, let’s first talk about the difference between module.exports and exports.

First, we use a new module to conduct a simple verification

console.log(module.exports === exports); // true

We can find that module.exports and epxorts actually point to the same Reference variables.

demo1

// a模块
module.exports.text = 'xxx';
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {text: 'xxx', value: 2}

This also verifies why module.exports and are passed in demo1 above. exportsAdd attributes, both exist when the module is introduced, because both ultimately add attributes to the same reference variable. Based on this demo, we can draw the conclusion: module.exports and exports point to the same reference variable

demo2

// a模块
module.exports = {
  text: 'xxx'
}
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {text: 'xxx'}

In the above demo example, module.exports The values ​​were reassigned, and exports added attributes, but after the module was introduced, the value defined by module.exports was finally exported. It can be concluded that the noed module finally What is exported is module.exports, and exports is just a reference to module.exports, similar to the following code:

exports = module.exports = {};
(function (exports, module) {
  // a模块里面的代码
  module.exports = {
    text: 'xxx'
  }
  exports.value = 2;

  console.log(module.exports === exports); // false
})(exports, module)

Due to function execution, exports only It is a reference to the variable corresponding to the original module.exports. When assigning a value to module.exports, the variable corresponding to exports is the same as the latest module .exports is not the same variable

require method

##requireThe process of introducing a module is mainly divided into the following steps:

  • 解析文件路径成绝对路径
  • 查看当前需要加载的模块是否已经有缓存, 如果有缓存, 则直接使用缓存的即可
  • 查看是否是 node 自带模块, 如 http,fs 等, 是就直接返回
  • 根据文件路径创建一个模块对象
  • 将该模块加入模块缓存中
  • 通过对应的文件解析方式对文件进行解析编译执行(node 默认仅支持解析.js,.json, .node后缀的文件)
  • 返回加载后的模块 exports 对象

Quickly understand the module system in Nodejs in one article

Module.prototype.require = function(id) {
  // ...
  try {
    // 主要通过Module的静态方法_load加载模块 
    return Module._load(id, this, /* isMain */ false);
  } finally {}
  // ...
};
// ...
Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;
  // ...
  // 解析文件路径成绝对路径
  const filename = Module._resolveFilename(request, parent, isMain);
  // 查看当前需要加载的模块是否已经有缓存
  const cachedModule = Module._cache[filename];
  // 如果有缓存, 则直接使用缓存的即可
  if (cachedModule !== undefined) {
    // ...
    return cachedModule.exports;
  }

  // 查看是否是node自带模块, 如http,fs等, 是就直接返回
  const mod = loadNativeModule(filename, request);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // 根据文件路径初始化一个模块
  const module = cachedModule || new Module(filename, parent);

  // ...
  // 将该模块加入模块缓存中
  Module._cache[filename] = module;
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  // ...
  // 进行模块的加载
  module.load(filename);

  return module.exports;
};

至此, node 的模块原理流程基本过完了。目前 node v13.2.0 版本起已经正式支持 ESM 特性。

__filename, __dirname

在接触 node 中,你是否会困惑 __filename, __dirname是从哪里来的, 为什么会有这些变量呢? 仔细阅读该章节,你会对这些有系统性的了解。

  • 顺着上面的 require 源码继续走, 当一个模块加载时, 会对模块内容读取
  • 将内容包裹成函数体
  • 将拼接的函数字符串编译成函数
  • 执行编译后的函数, 传入对应的参数
Module.prototype._compile = function(content, filename) {
  // ...
  const compiledWrapper = wrapSafe(filename, content, this);
  // 
  result = compiledWrapper.call(thisValue, exports, require, module,
                                    filename, dirname);
  
  // ...
  return result;
};
function wrapSafe(filename, content, cjsModuleInstance) {
  // ...
  const wrapper = Module.wrap(content);
  // ...
}
let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

ObjectDefineProperty(Module, 'wrap', {
  get() {
    return wrap;
  },

  set(value) {
    patched = true;
    wrap = value;
  }
});

综上, 也就是之所以模块里面有__dirname,__filename, module, exports, require这些变量, 其实也就是 node 在执行过程传入的, 看完是否解决了多年困惑的问题^_^

NodeJS 中使用 ES Modules

  • package.json增加"type": "module"配置
// test.mjs
export default {
	a: 'xxx'
}
// import.js
import a from './test.mjs';

console.log(a); // {a: 'xxx'}

import 与 require 两种机制的区别

较明显的区别是在于执行时机:

  • ES 模块在执行时会将所有import导入的模块会先进行预解析处理, 先于模块内的其他模块执行
// entry.js
console.log('execute entry');
let a = require('./a.js')

console.log(a);

// a.js
console.log('-----a--------');

module.exports = 'this is a';
// 最终输出顺序为:
// execute entry
// -----a--------
// this is a
// entry.js
console.log('execute entry');
import b from './b.mjs';

console.log(b);

// b.mjs
console.log('-----b--------');

export default 'this is b';
// 最终输出顺序为:
// -----b--------
// execute entry
// this is b
  • import 只能在模块的顶层,不能在代码块之中(比如在if代码块中),如果需要动态引入, 需要使用import()动态加载;

ES 模块对比 CommonJS 模块, 还有以下的区别:

  • 没有 requireexportsmodule.exports

    在大多数情况下,可以使用 ES 模块 import 加载 CommonJS 模块。(CommonJS 模块文件后缀为 cjs) 如果需要引入.js后缀的 CommonJS 模块, 可以使用module.createRequire()在 ES 模块中构造require函数

// test.cjs
export default {
a: 'xxx'
}
// import.js
import a from './test.cjs';

console.log(a); // {a: 'xxx'}
// test.cjs
export default {
a: 'xxx'
}
// import.js
import a from './test.cjs';

console.log(a); // {a: 'xxx'}
// test.cjs
export default {
a: 'xxx'
}
// import.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

// test.js 是 CommonJS 模块。
const siblingModule = require('./test');
console.log(siblingModule); // {a: 'xxx'}
  • 没有 __filename 或 __dirname

    这些 CommonJS 变量在 ES 模块中不可用。

  • 没有 JSON 模块加载

    JSON 导入仍处于实验阶段,仅通过 --experimental-json-modules 标志支持。

  • 没有 require.resolve

  • 没有 NODE_PATH

  • 没有 require.extensions

  • 没有 require.cache

ES 模块和 CommonJS 的相互引用

在 CommonJS 中引入 ES 模块

由于 ES Modules 的加载、解析和执行都是异步的,而 require() 的过程是同步的、所以不能通过 require() 来引用一个 ES6 模块。

ES6 提议的 import() 函数将会返回一个 Promise,它在 ES Modules 加载后标记完成。借助于此,我们可以在 CommonJS 中使用异步的方式导入 ES Modules:

// b.mjs
export default 'esm b'
// entry.js
(async () => {
	let { default: b } = await import('./b.mjs');
	console.log(b); // esm b
})()

在 ES 模块中引入 CommonJS

在 ES6 模块里可以很方便地使用 import 来引用一个 CommonJS 模块,因为在 ES6 模块里异步加载并非是必须的:

// a.cjs
module.exports = 'commonjs a';
// entry.js
import a from './a.cjs';

console.log(a); // commonjs a

至此,提供 2 个 demo 给大家测试下上述知识点是否已经掌握,如果没有掌握可以回头再进行阅读。

demo module.exports&exports

// a模块
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {value: 2}

demo module.exports&exports

// a模块
exports = 2;

// b模块代码
let a = require('./a');

console.log(a); // {}

require&_cache 模块缓存机制

// origin.js
let count = 0;

exports.addCount = function () {
	count++
}

exports.getCount = function () {
	return count;
}

// b.js
let { getCount } = require('./origin');
exports.getCount = getCount;

// a.js
let { addCount, getCount: getValue } = require('./origin');
addCount();
console.log(getValue()); // 1
let { getCount } = require('./b');
console.log(getCount()); // 1

require.cache

根据上述例子, 模块在 require 引入时会加入缓存对象require.cache中。 如果需要删除缓存, 可以考虑将该缓存内容清除,则下次require模块将会重新加载模块。

let count = 0;

exports.addCount = function () {
	count++
}

exports.getCount = function () {
	return count;
}

// b.js
let { getCount } = require('./origin');
exports.getCount = getCount;

// a.js
let { addCount, getCount: getValue } = require('./origin');
addCount();
console.log(getValue()); // 1
delete require.cache[require.resolve('./origin')];
let { getCount } = require('./b');
console.log(getCount()); // 0

结语

至此,本文主要介绍了 Node 中基于CommonJS实现的模块化机制,并且通过源码的方式对模块化的整个流程进行了分析,有关于模块的介绍可查看下面参考资料。有疑问的欢迎评论区留言,谢谢。

参考资料

CommonJS 模块

ES Module

##Original address: https://juejin.cn/post/7007233910681632781


Author:

Slow Eat Ah

For more programming-related knowledge, please visit:

Programming Video! !

The above is the detailed content of Quickly understand the module system in Nodejs in one article. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete
Previous article:How to use es6 setNext article:How to use es6 set