[NodeJS] JavaScript模块化

JavaScript诞生于1995年,一开始只是用于编写简单的脚本。

随着前端开发任务越来越复杂,JavaScript代码也越来越复杂,全局变量冲突、依赖管理混乱等问题变得十分突出,模块化成为一个必不可少的功能。

模块化发展史与方案对比

YUI 与 JQuery

2006年,雅虎开源了组件库YUI Library,使用类似于Java的命名空间的方式来管理模块:

YUI.util.module.doSomthing();

同年,JQuery发布,使用IIFE和闭包的方法创建私有作用域,避免全局变量污染,这种管理模块的方法流行了一段时间。

(function(root) {// 模块内部变量和函数var data = 'Module Data';function getData() {return data;}// 将模块接口挂载到全局对象上root.myModule = {getData: getData};
})(window);// 使用模块
console.log(myModule.getData()); // 输出: Module Data

当一个模块依赖于其它模块时:(ModuleB依赖于ModuleA和ModuleC)

(function(root, moduleA, moduleC) {// 模块B代码
})(window, window.moduleA, window.moduleC);

这种方法有以下缺点:

  1. 模块挂载到了全局对象上,仍然可能存在冲突;
  2. 缺乏标准化,不同程序员对这种方案的实现可能不同;
  3. 依赖管理困难,当依赖模块量比较大时,手动传递参数容易出错。

ServerJS, CommonJS 和 Node.js

2009年1月,Mozilla 的工程师制定了一套JavaScript模块化的标准规范,取名为ServerJS,规范版本为Modules/0.1。

同年4月,ServerJS更名为CommonJS

ServerJS最初用于服务端的JS模块化,用于辅助自动化测试工作。

在Node.js出现之前也有运行于服务端的JS,叫做Netscape Enterprise Server,它并不像Node.js,后者拥有访问操作系统文件系统,以及网络I/O等能力;而前者更像是早期的php,只是用于在服务端填充模板。更详细的介绍可以看👉Server-side JavaScript a decade before Node.js with Netscape LiveWire - DEV Community
下面这段代码摘自链接里的文章。

<!-- Welcome to mid-90s HTML. 
Tags are SCREAMED, because everybody is very excited about THE INTERNET. -->
<HTML><HEAD><TITLE>My awesome web app</TITLE></HEAD><BODY>  <H1><SERVER>/* This tag and its content will be processed on the server side,and replaced by whatever is passed to `write()` before being sent to the client. */if(client.firstname != null) {write("Hello " + client.firstname + " !")  }else {write("What is your name?")}</SERVER></H1><FORM METHOD="post" ACTION="app.html"><P><LABEL FOR="firstname">Your name</LABEL><INPUT TYPE="text" NAME="firstname"/>        </P><P><INPUT TYPE="submit" VALUE="Send"/></P></FORM></BODY>  
</HTML>

同年8月,Node.js闪亮登场,但是还没有包管理器,外部依赖需要手动下载到项目文件夹中。

而包管理器的设计则需要考虑到使用什么模块化方案。Node.js的作者最终选择了同年刚提出的CommonJS作为npm的模块化方案,此时的CommonJS已经采用了Modules/1.0版本的标准规范:Modules/1.0 - CommonJS Spec Wiki

需要注意到此时的CommonJS模块化方案是针对运行在服务端的Node.js准备的,浏览器的JS出于以下原因并不能使用CommonJS:

  1. 同步加载模块:CommonJS使用同步的require函数加载依赖模块,但是在浏览器环境中,网络请求加载模块文件是异步操作,无法同步执行;
  2. 缺乏模块依赖管理:Node.js可以直接从本地文件系统或node_modules文件夹加载模块,而浏览器无法自动处理模块的查找和加载;
  3. 没有内置模块加载器:Node.js有内置的模块加载器,可以处理模块的解析、加载和缓存,但是浏览器没有类似的内置机制。

社区意识到为了可以在浏览器环境中使用CommonJS,必须制订新的标准规范,但是这个时候社区人员的思路出现了分歧,开始出现了不同的流派,也是从这个时候开始,出现了很多不同的模块化方案。

方向1:browserify

社区的其中一种思路是在打包代码的时候将CommonJS的模块语法转换成浏览器可以运行的代码。

相关的规范是 Modules/Transport规范,用于规定模块如何转译。

👉Modules/Transport - CommonJS Spec Wiki

这个方向的著名产物是 Browserify

方向2:AMD

这种思路是:由于CommonJS的require函数是同步加载模块的,没办法在浏览器环境应用,那么我们就异步地加载模块

AMD是 Async Module Definition的简称,即异步模块定义。

James Burke在2009年9月开发了RequireJS这个模块加载器,可以异步地加载依赖。

这个方向产生了AMD标准规范:Modules/AsynchronousDefinition - CommonJS Spec Wiki

除了使用require引入依赖,还需要一个新的全局函数:define来注册依赖。

使用方法大致如下:

// module1.js
define(function() {return {data: 'Module 1 Data',getData: function() {return this.data;}};
});// main.js
require(['module1'], function(module1) {console.log(module1.getData()); // 输出: Module 1 Data
});

方向3:CMD

2011年4月,阿里巴巴的玉伯借鉴了CommonJSAMD等模块化方案后,写出了SeaJS,在此基础上,提出了 Common Module Definition 简称CMD规范。

CMD规范的主要内容和AMD规范差不多,主要差别是CMD保留了CommonJS中最重要的延迟加载就近声明特性。

延迟加载

CMD 模块依赖是延迟加载的,只有在需要时才会加载依赖的模块。而 AMD 在定义模块的时候就需要加载依赖的模块了。

就近声明

CMD在define的回调函数中使用require函数声明依赖,而AMD中模块的依赖是在函数参数列表中显式声明的。


AMD示例

define(['dep1', 'dep2'], function(dep1, dep2) {dep1.doSomething();dep2.doSomething();
});

CMD示例

define(function(require, exports, module) {// 依赖在使用时才加载var dep1 = require('./dep1');var dep2 = require('./dep2');exports.foo = function() {dep1.doSomething();dep2.doSomething();};
});

尝试统一:UMD

2014年9月,UMD被提出(Universal Module Definition),本质上不是模块化方案,只是将CommonJS和AMD进行结合。

通过检查全局作用域下是否存在exportsdefine方法以及全局对象上是否定义了依赖,来决定采用哪一种方法加载模块。

官方方案:ES6

到了2016年,ES6标准发布,引入了importexport两个关键字,提供了 ES Module 模块化方案。

ESM可以在编译时进行静态解析和优化,并且在后续版本中支持了动态导入(异步)。

时间线图

modules.drawio

总结

模块方案 特点 优点 缺点
IIFE + 闭包 IIFE 结合闭包,实现模块化 简单直接,不依赖特定标准或工具 依赖管理复杂,易产生全局变量冲突,代码维护性和可读性较差
CommonJS 用于服务器端,同步加载 简单直观,广泛支持 不适合浏览器,需要工具转换
AMD 设计用于浏览器,异步加载 提高性能,依赖并行加载 语法复杂,代码冗长
CMD 类似 AMD,更灵活,支持延迟加载 灵活性高,依赖就近声明和延迟加载 使用较少,需要工具支持
UMD 兼容 CommonJS 和 AMD,通用模块定义 兼容性强,可在不同环境中使用 代码复杂,处理环境差异
ESM ES6 标准模块系统,静态分析 原生支持,静态分析,语法简洁 无?

目前主流的方案是CommonJS和ESM,前者是因为历史原因,使用范围广,至今仍有许多代码使用CommonJS;而后者是官方给出的方案,简洁高效。

现在有许多转译工具例如:babeltypescript等等,可以将CommonJS转换成浏览器可以执行的形式,因此在前端项目中使用哪一种方案已经不是特别重要了。


Node.js 中的 CommonJS

在Node.js中每个文件都被看做一个模块,内部成员不会污染全局作用域,可以使用require函数引入其它模块,也可以使用module.exports导出。

模块类型

  • Node.js核心模块:fshttpnet等等;
  • 开发者模块:本地的文件;
  • 第三方模块:通常是使用 npm 安装到 node_modules 文件夹下的模块。

Node.js只会将.js.json.node文件视为模块,其它文件格式需要额外安装其它依赖进行转换,比如.ts

require执行过程

解析文件路径 Resolve

require作为一个函数,传入一个字符串,首先要解析这个字符串代表哪一个模块。

这个解析的过程是调用了require.resolve()函数。

解析过程如下:

  1. 尝试查询该名称的Node.js核心模块;
  2. 如果以./或者../开头,那么尝试解析URL,加载开发者模块(即查询文件);
  3. 如果找不到 2. 的文件,那么尝试将字符串视为文件夹,查找文件夹内部的index.js
  4. 如果还没找到,则去node_modules文件夹内部查找指定模块并加载;
  5. 如果还没找到,则抛出异常。

对于同名但不同格式的文件,匹配顺序是:js > json > node

对于node_modules里的第三方模块(文件夹),是先去查找package.json里的main字段配置的入口文件,如果没有package.json文件或者没有配置main字段,则找文件夹内部的index

其实还有很多细节这里忽略了,可以去官网看详细的匹配规则伪代码(很长):Modules: CommonJS modules | Node.js v22.4.1 Documentation (nodejs.org)

包装 Wrapping

经过上一个步骤我们已经查找到了模块对应的文件。

众所周知,代码文件都是文本文件,可以将代码读出得到一个字符串。

在这一步,模块的代码会被包装成一个函数,因此可以访问特殊对象(注入)

(function(exports, require, module, __filename, __dirname){// ... Module Code
})();

好处:模块的顶层代码不会泄露。

我们可以写一个简单的小案例来测试这个包装步骤:

// module.js
console.log(arguments);// ========================================
// index.js
require('./module');

我们在index.js中引入module.js这个模块,而在module.js中,我们只做一件很简单的事情,那就是直接输出arguments对象。

执行node index.js,得到以下结果:

image-20240709165000660

这五个对象分别就是exportsrequiremodule__filename__dirname

这个文件目前没有导出内容,所以可以看到第一个对象,和第三个对象module.exports是空对象。

接下来我们尝试导出点内容:

// module.js
exports.msg = 'msg from exports'module.exports = {msg: 'msg from module.exports'
}console.log(arguments);//==============================================
// index.js
const foo = require('./module');console.log('index.js receive: ' + foo.msg);
image-20240709172434574

在模块加载的时候,exports默认指向module.exports,也就是说我们要导出内容的时候,既可以通过exports,也可以通过module.exports最终require返回的是module.exports

通常在这个环节如果发生了意想不到的情况,大部分问题都是由赋值操作导致的。

exports如果被重新赋值,那么就和module.exports不是同一个引用了。

exports和module.exports的使用注意事项

特性 module.exports exports
初始化 默认为一个空对象 {},可以被赋值为任何其他对象或值。 在模块加载时,默认与 module.exports 相同,可以通过赋值语句修改其引用。
赋值方式 直接赋值为需要导出的对象或值,可以是任何有效的 JavaScript 对象或函数。 通过 exports 的属性进行赋值,例如 exports.foo = ...。
重新赋值影响 若直接赋值新对象给 module.exports,会覆盖原有的导出对象。 若重新赋值新对象给 exports,只是修改了 exports 的引用,不会影响 module.exports。
适用场景 适合导出单个对象、函数或类。 适合在导出多个方法或属性时的简便语法。
注意事项 赋值给 module.exports 必须在模块加载时立即完成,不能在回调中完成赋值操作。 赋值给 exports 只是修改了 exports 变量的引用,而不会改变 module.exports 的引用,因此需要注意不要混淆或误用。

require对象

image-20240709175811687

require是一个函数,当然我们都知道在JS中函数也是对象,require携带了一些很重要的属性:

  • resolve一个函数,require就是使用这个函数来将模块字符串解析为文件的具体路径的;

  • main:我们知道每一个.js文件既可以被视为模块,又可以被视为主程序使用node指令启动,那么被node xxx.js执行的这个xxx.js的相关信息,就被记录到了main这个对象里,在这个案例里是index.js

  • extensions:以前用来将非js模块加载到Node.js中,已废弃,不要使用,否则可能导致bug或者性能下降

  • cache:模块的缓存记录,这里有个细节,当前模块还没有执行完成,但是已经存在一条缓存记录了,只不过当前的缓存对象可能不是最终结果(取决于console.logmodule.exports的执行顺序),这个现象在下文的循环依赖问题会重点讨论。

    注意:缓存的key是文件路径,而不是传给require的那个字符串,因此每次调用require引入已缓存的模块仍需要走一次resolve环节获得文件的路径。

执行 Execution

由Node.js运行时执行模块代码。

返回 Exports

require函数返回值是module.exports

缓存 Cache

对于一个模块来说,包装和执行过程只会被执行一次,然后会被缓存。

后续对相同模块的require只会走一个解析文件路径的环节,然后就返回缓存。

循环依赖问题 Cycles

graph TDindex.js --> a.jsa.js --> b.jsb.js --> a.js

如图,模块a和模块b相互依赖,为了避免无穷循环,Node.js使用其缓存机制来处理这种问题。

官方文档案例

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'); COPY

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'); COPY

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); COPY

输出顺序

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

解析

  1. main.js执行require('./a.js')的时候,就已经创建一个Module对象并添加到缓存里了,只不过此时a的Module对象的exports还是空的;
  2. 接下来执行a.js的模块代码,exports.done赋值为false
  3. 执行到require('./b.js'),发现没有相应的缓存,创建b的Module对象并添加到缓存,此时b的exports还是空对象;
  4. 执行b.js的模块代码,exports.done赋值为false
  5. 执行到require('./a.js'),发现有缓存,直接返回模块a的module.exports对象,此时的donefalse
  6. 注意,此时是直接返回模块a的module.exports对象,没有去重新执行a.js的代码,因此避免了循环。
  7. 后续就是b.js完成了模块代码执行,a.js完成剩余代码执行,main.js完成剩余代码执行,程序结束。
graph TDmain.js ==> a.jsa.js ==> b.jsb.js ==> checkcheck -.-> |无缓存|a.jscheck ==> |有缓存|模块a的module.exports

参考文章

[1] 《编程时间简史系列》JavaScript 模块化的历史进程 - 编程时间简史 - SegmentFault 思否

[2] Modules: CommonJS modules | Node.js v22.4.1 Documentation (nodejs.org)

[3] javascript - 深入Node.js的模块加载机制,手写require函数 - 进击的大前端 - SegmentFault 思否

[4] JavaScript 模块的循环加载 - 阮一峰的网络日志 (ruanyifeng.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/741345.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

论文阅读: 面向Planning的端到端智驾Planning-oriented Autonomous Driving

设计一个更优的、可理解的、面向最终目标的框架。基于这个面向Planning的思想,他们提出了 Unified Autonomous Driving (UniAD)方案,一种新的自动驾驶框架。这个方案从全局视角出发,让智驾的各个模块特征提取可以互相补充,各个任务之间可以通过统一的查询接口通信。在此基础…

基于FileZilla上传、下载服务器数据的方法

本文介绍FileZilla软件的下载、配置与使用方法~本文介绍FileZilla软件的下载、配置与使用方法。在之前的博客中,我们提到了下载高分遥感影像数据需要用到FTP(文件传输协议,File Transfer Protocol)软件FileZilla;这一软件用以在自己的电脑与服务器之间相互传输数据,在进行…

text1

sad das dasdasdgrdgt是g天热共同的sgtd发广泛的撒旦撒撒大大大叔

TextCNN: Convolutional Neural Networks for Sentence Classification

本文是CNN应用在NLP领域的开山之作。TextCNN的成功并不是网络结构的成功,而是通过引入已经训练好的词向量在多个数据集上达到了超越benchmark的表现,证明了构造更好的embedding,是提升NLP各项任务的关键能力。作者做了一系列实验,这些实验使用卷积神经网络(CNN)在预训练的…

Matlab图片的处理

上一章我们介绍了奇异值分解的理论原理,这一章我们使用奇异值分解来压缩图片 目录一、RGB模式(1) 灰色图片与彩色图片二、matlab进行图片压缩1.参数分析2.读取图像文件并进行转换3.进行奇异值分解4.将压缩后的图片保存5.实例演示(1)原图:(2)进行处理(3)处理后————保…

AI绘图实践-用人工智能生图助力618大促

现在各种AI大模型大行其道,前有GhatGPT颠覆了我们对对话型AI的原有印象,后有Sora文生视频,让我们看到了利用AI进行创意创作的无限可能性。如今各大公司和团队都争相提出自己的大模型,各种网页端和软件应用也极大地降低了我们使用AI作为生产力的门槛。 我这次就为大家带来使…

托寄物智能识别——大模型在京东快递物流场景中的应用与落地

一、前言 在现代物流场景中,包裹信息的准确性和处理效率至关重要。当前,京东快递在邮寄场景中面临着日益丰富的寄递品类和多样化的个性化需求。本文将深入探讨托寄物智能识别——大模型在京东快递物流场景中的应用与落地,分析其产生背景、应用效果及未来发展方向。 二、背景…

Golang 切片作为函数参数传递的陷阱与解答

作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。 GitHub : https://github.com/af913337456/ 出版的书籍:《1.0-区块链DApp开发实战》 《2.0-区块链DApp开发:基于公链》例子切片作为函数参数传递的是值 用来误导切片作为函数参数传递的是引用 函数内切片 append 引…

暑假读论文总结

7.8SAM-G 待填7.9MAE(Masked Autoencoders Are Scalable Vision Learners) 来源:CVPR 2022 在视觉领域应用 auto encoder 的比较早的工作了,是自监督学习。 主要内容是在原图中选择若干个 patch 进行遮挡(patch 通常选的很多,~75%),通过 encoder - decoder 进行复原。e…

设计模式学习(二)工厂模式——抽象工厂模式+注册表

介绍抽象工厂模式初版代码的改进方案目录前言使用简单工厂改进使用注册表改进参考文章 前言 在上一篇文章中我们提到了抽象工厂模式初版代码的一些缺点:①客户端违反开闭原则②提供方违反开闭原则。本文将针对这两点进行讨论 使用简单工厂改进 对于缺点①,我们可以使用简单工…

服务器怎么连接?服务器远程连接图文教程

服务器操作系统可以实现对计算机硬件与软件的直接控制和管理协调,任何计算机的运行离不开操作系统,服务器也一样,服务器操作系统主要分为四大流派:Windows Server、Netware、Unix和Linux 今天飞飞就给你们分享下常用的Windows、Linux、Unix三种系统的远程连接图文操作方法服…