一文快速了解Nodejs中的模块系统

本篇文章带大家了解一下nodejs中的模块系统,希望对大家有所帮助!

一文快速了解Nodejs中的模块系统

模块化的背景

早期 JavaScript 是为了实现简单的页面交互逻辑, 但随着时代发展, 浏览器不单单仅只能呈现简单交互, 各种各样的网站开始大放光彩。随着网站开始变得复杂化,前端代码日渐增多,相对比起其他静态语言,JavaScript 缺少模块化的弊端开始暴露出来,如命名冲突。因此为了方便前端代码的维护和管理,社区开始了对模块化规范的定义。在这过程中,出现了很多的模块化规范,如CommonJS, AMD, CMD, ES modules,本文章主要讲解Node中根据CommonJS实现的模块化。

CommonJS 规范

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

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

Node 模块分类

核心模块: 如 fs,http,path 等模块, 这些模块不需要安装, 在运行时已经加载在内存中。【推荐学习:《nodejs》】第三方模块: 通过安装存放在 node_modules 中。自定义模块: 主要是指 file 模块,通过绝对路径或者相对路径进行引入。

Module 对象

我们上面说过,一个文件就是一个模块,且通过一个 module 对象来描述当前模块信息,一个 module 对象对应有以下属性:- id: 当前模块的id- path: 当前模块对应的路径- exports: 当前模块对外暴露的变量- parent: 也是一个module对象,表示当前模块的父模块,即调用当前模块的模块- filename: 当前模块的文件名(绝对路径), 可用于在模块引入时将加载的模块加入到全局模块缓存中, 后续引入直接从缓存里进行取值- loaded: 表示当前模块是否加载完毕- children: 是一个数组,存放着当前模块调用的模块- paths: 是一个数组,记录着从当前模块开始查找node_modules目录,递归向上查找到根目录下的node_modules目录下

module.exports 与 exports

说完CommonJS规范,我们先讲下module.exports与exports的区别。

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

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

登录后复制

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

demo1

// a模块module.exports.text = 'xxx';exports.value = 2;// b模块代码let a = require('./a');console.log(a); // {text: 'xxx', value: 2}

登录后复制

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

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引入模块的过程主要分为以下几步:

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

1.png

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) { ',  '});'];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.mjsexport default {a: 'xxx'}// import.jsimport a from './test.mjs';console.log(a); // {a: 'xxx'}

登录后复制

import 与 require 两种机制的区别

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

ES 模块在执行时会将所有import导入的模块会先进行预解析处理, 先于模块内的其他模块执行

// entry.jsconsole.log('execute entry');let a = require('./a.js')console.log(a);// a.jsconsole.log('-----a--------');module.exports = 'this is a';// 最终输出顺序为:// execute entry// -----a--------// this is a

登录后复制

// entry.jsconsole.log('execute entry');import b from './b.mjs';console.log(b);// b.mjsconsole.log('-----b--------');export default 'this is b';// 最终输出顺序为:// -----b--------// execute entry// this is b

登录后复制import 只能在模块的顶层,不能在代码块之中(比如在if代码块中),如果需要动态引入, 需要使用import()动态加载;

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

没有 require、exports 或 module.exports

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

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

登录后复制登录后复制

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

登录后复制登录后复制

// test.cjsexport default {a: 'xxx'}// import.mjsimport { 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.mjsexport 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.cjsmodule.exports = 'commonjs a';// entry.jsimport 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.jslet count = 0;exports.addCount = function () {count++}exports.getCount = function () {return count;}// b.jslet { getCount } = require('./origin');exports.getCount = getCount;// a.jslet { addCount, getCount: getValue } = require('./origin');addCount();console.log(getValue()); // 1let { 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.jslet { getCount } = require('./origin');exports.getCount = getCount;// a.jslet { addCount, getCount: getValue } = require('./origin');addCount();console.log(getValue()); // 1delete require.cache[require.resolve('./origin')];let { getCount } = require('./b');console.log(getCount()); // 0

登录后复制

结语

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

参考资料

nodejs

nodejs

原文地址:https://juejin.cn/post/7007233910681632781作者:慢吃啊

更多编程相关知识,请访问:nodejs!!

以上就是一文快速了解Nodejs中的模块系统的详细内容,更多请关注【创想鸟】其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。

发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/2707990.html

(0)
上一篇 2025年3月7日 20:16:44
下一篇 2025年2月18日 04:28:04

AD推荐 黄金广告位招租... 更多推荐

相关推荐

  • linux 命令怎么安装nodejs

    linux命令安装nodejs的方法:1、选择一个合适的nodejs版本下载;2、通过“tar -xvf node-v10.6.0-linux-x64.tar.xz”命令将安装包上传到指定位置即可。 本文操作环境:linux5.9.8系统、…

    2025年3月7日
    200
  • nodejs怎么更改路径

    nodejs更改路径的方法:1、在安装目录下新建node_global等文件夹;2、在cmd窗口下执行“npm install gulp -g”命令;3、在环境变量配置的用户变量中增加一个配置即可。 本文操作环境:windows7系统、no…

    2025年3月7日
    200
  • nodejs如何升级

    升级nodejs的方法:1、利用n模块,使用“n stable”命令可升级到最新稳定版、“n latest”命令可升级到最新版;2、利用NVM工具,使用“nvm install lastest”可升级到最新版。 本教程操作环境:centos…

    2025年3月7日
    200
  • 快速搞懂Node.js中的性能指标

    本篇文章带大家了解一下node.js的性能指标,希望对大家有所帮助! 对于我们前端工程师来说,掌握Node.js应用开发是我们走上资深/专家的一条必经之路。此外Node.js是一门服务端语言,我们不仅要能够完成开发任务,而且更应该要关注服务…

    2025年3月7日 编程技术
    200
  • 浅谈如何使用Nodejs创建访问日志记录的中间件

    本篇文章给大家分享一下使用node.js实现接口访问日志记录功能,希望对大家有所帮助! 中间件-访问日志 目标 利用中间件技术,写一个用来记录访问日志的中间件 中间件 middleware, 中间件是一个特殊的url地址处理函数,它被当作 …

    2025年3月7日
    200
  • 如何解决nodejs express安装失败问题

    nodejs express安装失败的解决办法:1、切换到nodejs安装目录下;2、执行“npm install express”命令即可。 本文操作环境:windows7系统、nodejs10.16.2版、Dell G3电脑。 如何解决…

    2025年3月7日
    200
  • Nodejs进阶学习:深入了解异步I/O和事件循环

    本篇文章是nodejs的进阶学习,带大家详细了解一下nodejs中的异步i/o和事件循环,希望对大家有所帮助! 本文讲详细讲解 nodejs 中两个比较难以理解的部分异步I/O和事件循环,对 nodejs 核心知识点,做梳理和补充。【推荐学…

    2025年3月7日 编程技术
    200
  • 手把手教你使用Node.js进行TCP网络通信(实践)

    如何使用node.js进行tcp网络通信?下面本篇文章就来带大家搞懂使用node.js进行tcp网络通信的方法,希望对大家有所帮助! 摘要: 网络是通信互联的基础,Node.js提供了net、http、dgram等模块,分别用来实现TCP、…

    2025年3月7日 编程技术
    200
  • 浅谈离线安装全局node模块的方法

    怎么离线安装全局 node 模块吗?下面本篇文章给大家介绍一下离线安装全局node模块的方法,希望对大家有所帮助! 大家平时是怎么离线安装 node 模块的呢?【推荐学习:《node》】 前言 在平时的开发中,我们一般都是通过在线安装的方式…

    2025年3月7日 编程技术
    200
  • 浅谈NodeJS获取程序退出码的方法

    如何获取 nodejs 程序退出码?下面本篇文章给大家介绍一下获取 node.js 程序退出码的方法以及退出码枚举,希望对大家有所帮助! 想要退出正在运行的 NodeJS 程序,我们既可以通过 Ctrl + C  的方式,也可以通过 pro…

    2025年3月7日
    200

发表回复

登录后才能评论