>  기사  >  웹 프론트엔드  >  한 기사로 Nodejs의 모듈 시스템을 빠르게 이해하세요

한 기사로 Nodejs의 모듈 시스템을 빠르게 이해하세요

青灯夜游
青灯夜游앞으로
2021-09-14 10:32:102362검색

이 기사는 Nodejs의 모듈 시스템을 안내합니다. 도움이 되기를 바랍니다!

한 기사로 Nodejs의 모듈 시스템을 빠르게 이해하세요

모듈형 배경

초기 JavaScript는 간단한 페이지 상호작용 로직을 구현하는 데 사용되었지만, 시대가 발전하면서 브라우저는 단순한 상호작용을 표시할 수 있을 뿐만 아니라 다양한 웹사이트도 빛을 발하기 시작했습니다. 다른 정적 언어에 비해 웹사이트가 복잡해지고 프런트엔드 코드가 늘어나면서 이름 충돌 등 JavaScript의 모듈성 부족이 드러나기 시작합니다. 따라서 프런트 엔드 코드의 유지 관리 및 관리를 용이하게 하기 위해 커뮤니티에서는 모듈식 사양을 정의하기 시작했습니다. 이 과정에서 CommonJS, AMD, CMD, ES 모듈과 같은 많은 모듈 사양이 등장했습니다. 이 글에서는 CommonJS를 기반으로 Node에 구현된 모듈화를 주로 설명합니다. CommonJS, AMD, CMD, ES modules,本文章主要讲解Node中根据CommonJS实现的模块化。

CommonJS 规范

首先,在 Node 世界里,模块系统是遵守CommonJS规范的,CommonJS规范里定义,简单讲就是:

  • 每一个文件就是一个模块
  • 通过module对象来代表一个模块的信息
  • 通过exports用来导出模块对外暴露的信息
  • 通过require

  • CommonJS 사양

우선 Node 세계에서는 모듈 시스템이 CommonJS 사양, CommonJS를 준수합니다. code> 사양 정의, 간단히 말하면:

노드 모듈 분류

를 통해 모듈을 참조합니다. 핵심 모듈: fs, http, path 및 기타 모듈과 같은 모듈은 필요하지 않습니다. 런타임에 설치하려면 메모리에 이미 로드되어 있어야 합니다. [추천 학습: "

nodejs tutorial

"]타사 모듈: 설치를 통해 node_modules에 저장됩니다.

사용자 정의 모듈: 주로 절대 경로 또는 상대 경로를 통해 도입되는 파일 모듈을 말합니다. CommonJS规范,我们先讲下module.exportsexports的区别。

首先,我们用个新模块进行一个简单验证

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

可以发现,module.exportsepxorts实际上就是指向同一个引用变量。

demo1

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

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

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

从而也就验证了上面demo1中,为什么通过module.exportsexports添加属性,在模块引入时两者都存在, 因为两者最终都是往同一个引用变量上面进行属性的添加.根据该 demo, 可以得出结论: module.exportsexports指向同一个引用变量

demo2

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

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

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

上面的 demo 例子中,对module.exports进行了重新赋值, exports进行了属性的新增, 但是在引入模块后最终导出的是module.exports定义的值, 可以得出结论: noed 的模块最终导出的是module.exports, 而exports仅仅是对 module.exports 的引用, 类似于以下代码:

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

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

由于在函数执行中, exports 仅是对原module.exports对应变量的一个引用,当对module.exports进行赋值时,exports对应的变量和最新的module.exports并不是同一个变量

require 方法

require

🎜🎜모듈 객체🎜🎜🎜위에서 말했듯이 파일은 모듈이며, 모듈 객체는 현재 모듈 정보를 설명합니다. 모듈 객체는 다음 속성에 해당합니다. - id: 현재 모듈의 ID - path : 현재 모듈에 해당하는 경로 - 내보내기: 현재 모듈에 의해 외부 세계에 노출되는 변수 - parent: 현재 모듈의 상위 모듈, 즉 현재 모듈을 호출하는 모듈을 나타내는 모듈 개체이기도 합니다. - 파일 이름: 모듈이 도입될 때 로드된 모듈을 전역 모듈 캐시에 추가하는 데 사용할 수 있는 현재 모듈의 파일 이름(절대 경로)은 캐시에서 직접 값을 얻을 수 있습니다. - 로드됨: 현재 모듈이 로드되었는지 여부를 나타냅니다. - children: 현재 모듈이 호출하는 모듈을 저장하는 배열입니다. - paths : 현재 모듈부터 node_modules 디렉토리를 검색하여 루트 디렉토리🎜🎜🎜module.exports 및exports🎜🎜🎜에 대해 이야기한 후 node_modules 디렉토리까지 반복적으로 검색하는 내용을 기록한 배열입니다. CommonJS 사양, 먼저 module.exportsexports의 차이점에 대해 이야기해 보겠습니다. 🎜🎜먼저 새 모듈을 사용하여 간단한 검증을 수행합니다🎜
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;
};
🎜 module.exportsepxorts가 실제로 동일한 참조 변수를 가리키는 것을 확인할 수 있습니다. 🎜🎜🎜demo1🎜🎜
Module.prototype._compile = function(content, filename) {
  // ...
  const compiledWrapper = wrapSafe(filename, content, this);
  // 
  result = compiledWrapper.call(thisValue, exports, require, module,
                                    filename, dirname);
  
  // ...
  return result;
};
🎜따라서 위의 demo1에서 속성이 모듈의 module.exportsexports를 통해 추가되는 이유 둘 다 존재합니다. 둘 다 궁극적으로 동일한 참조 변수에 속성을 추가하기 때문에 이 데모를 기반으로 결론을 내릴 수 있습니다. module.exportsexports code>는 동일한 항목을 가리킵니다. 참조 변수🎜🎜🎜demo2🎜🎜<pre class="brush:js;toolbar:false;">function wrapSafe(filename, content, cjsModuleInstance) { // ... const wrapper = Module.wrap(content); // ... } let wrap = function(script) { return Module.wrapper[0] + script + Module.wrapper[1]; }; const wrapper = [ &amp;#39;(function (exports, require, module, __filename, __dirname) { &amp;#39;, &amp;#39;\n});&amp;#39; ]; ObjectDefineProperty(Module, &amp;#39;wrap&amp;#39;, { get() { return wrap; }, set(value) { patched = true; wrap = value; } });</pre>🎜위 데모 예시에서 <code>module.exports가 재할당되고, exports에 새로운 속성이 늘어나는데, 모듈이 도입된 후에는 module.exports에 의해 정의된 값이 최종적으로 내보내지는 반면, noed 모듈은 최종적으로 module.exports를 내보내는 반면, exports는 단지 내보내기만 한다는 결론을 내릴 수 있습니다. 다음 코드와 유사한 module.exports에 대한 참조: 🎜
// test.mjs
export default {
	a: &#39;xxx&#39;
}
// import.js
import a from &#39;./test.mjs&#39;;

console.log(a); // {a: &#39;xxx&#39;}
🎜함수 실행으로 인해 내보내기는 module.exports의 해당 변수에 대한 참조일 뿐입니다. code>module.exports, exports에 해당하는 변수는 최신 module.exports🎜

🎜require 방법🎜🎜🎜require모듈을 도입하는 과정은 크게 다음 단계로 나누어집니다.🎜
  • 解析文件路径成绝对路径
  • 查看当前需要加载的模块是否已经有缓存, 如果有缓存, 则直接使用缓存的即可
  • 查看是否是 node 自带模块, 如 http,fs 等, 是就直接返回
  • 根据文件路径创建一个模块对象
  • 将该模块加入模块缓存中
  • 通过对应的文件解析方式对文件进行解析编译执行(node 默认仅支持解析.js,.json, .node后缀的文件)
  • 返回加载后的模块 exports 对象

한 기사로 Nodejs의 모듈 시스템을 빠르게 이해하세요

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 = [
  &#39;(function (exports, require, module, __filename, __dirname) { &#39;,
  &#39;\n});&#39;
];

ObjectDefineProperty(Module, &#39;wrap&#39;, {
  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: &#39;xxx&#39;
}
// import.js
import a from &#39;./test.mjs&#39;;

console.log(a); // {a: &#39;xxx&#39;}

import 与 require 两种机制的区别

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

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

console.log(a);

// a.js
console.log(&#39;-----a--------&#39;);

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

console.log(b);

// b.mjs
console.log(&#39;-----b--------&#39;);

export default &#39;this is b&#39;;
// 最终输出顺序为:
// -----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: &#39;xxx&#39;
}
// import.js
import a from &#39;./test.cjs&#39;;

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

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

// test.js 是 CommonJS 模块。
const siblingModule = require(&#39;./test&#39;);
console.log(siblingModule); // {a: &#39;xxx&#39;}
  • 没有 __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 &#39;esm b&#39;
// entry.js
(async () => {
	let { default: b } = await import(&#39;./b.mjs&#39;);
	console.log(b); // esm b
})()

在 ES 模块中引入 CommonJS

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

// a.cjs
module.exports = &#39;commonjs a&#39;;
// entry.js
import a from &#39;./a.cjs&#39;;

console.log(a); // commonjs a

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

demo module.exports&exports

// a模块
exports.value = 2;

// b模块代码
let a = require(&#39;./a&#39;);

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

demo module.exports&exports

// a模块
exports = 2;

// b模块代码
let a = require(&#39;./a&#39;);

console.log(a); // {}

require&_cache 模块缓存机制

// origin.js
let count = 0;

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

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

// b.js
let { getCount } = require(&#39;./origin&#39;);
exports.getCount = getCount;

// a.js
let { addCount, getCount: getValue } = require(&#39;./origin&#39;);
addCount();
console.log(getValue()); // 1
let { getCount } = require(&#39;./b&#39;);
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(&#39;./origin&#39;);
exports.getCount = getCount;

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

结语

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

参考资料

CommonJS 模块

Module Original Address : https://juejin.cn/post/7007233910681632781

author : sloweat 더 많은 프로그래밍 관련 지식을 보려면 다음을 방문하십시오. !

위 내용은 한 기사로 Nodejs의 모듈 시스템을 빠르게 이해하세요의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제
이전 기사:es6 세트 사용 방법다음 기사:es6 세트 사용 방법