我最近开始学习 Node.js,遇到的第一个概念就是 模块。我已经做了几年前端开发,表面上也知道模块是什么,但从未深入理解它们。直到现在。
模块本质上只是代码文件。这是最简单的说法。但如果我们想深入了解模块究竟是什么、它们为什么存在以及如何使用它们,还有很多内容需要探讨。
Node.js 中的模块是任何应用程序的构建块。为什么?因为它们:
- 将代码组织到可管理的文件中
- 提供封装
- 防止全局命名空间污染
- 提高代码的可维护性和可重用性
让我们详细了解每一点。
1. 将代码组织到可管理的文件中
其核心是关注点分离,即每段代码只负责一件事,且仅负责这一件事。
我们来看一个简单的例子。假设我们有一些基本的数学运算,都放在一个文件 app.js 中:
function add(x, y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
function multiply(x, y) {
return x * y;
}
console.log(`Sum is: ${add(5, 3)}`); // Output: 8
console.log(`Subtraction is: ${subtract(5, 3)}`); // Output: 2
console.log(`Multiplication is: ${multiply(5, 3)}`); // Output: 15
Enter fullscreen mode Exit fullscreen mode
这是一个小而简单的例子,单独看并不混乱。但通过使用模块,我们可以让它更简洁、更有条理,并且随着增长更易于维护。
为此,我们将创建一个新文件 math.js,其唯一目的是处理数学运算:
function add(x, y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
function multiply(x, y) {
return x * y;
}
module.exports = { add, subtract, multiply };
Enter fullscreen mode Exit fullscreen mode
我们所做的只是将数学运算移到这个新文件中。要在应用程序的其他地方使用它们,我们需要导出它们,在 Node.js 中有不同的方法。这里我们使用 CommonJS,这是 Node.js 从一开始就默认使用的模块系统(与较新的标准 ES Modules 相对,本文不作介绍)。
现在,要在 app.js 中使用这些函数,我们使用 Node 内置的 require() 函数导入它们:
const { add, subtract, multiply } = require('./math');
console.log(`Sum is: ${add(5, 3)}`); // 8
console.log(`Subtraction is: ${subtract(5, 3)}`); // 2
console.log(`Multiplication is: ${multiply(5, 3)}`); // 15
Enter fullscreen mode Exit fullscreen mode
就这样,app.js 不再需要知道数学是如何完成的,只需要 require 这些函数并使用它们。数学逻辑存在于一个地方,只有一个明确的职责。
2. 封装
封装意味着隐藏某事物内部的工作细节,只暴露代码其他部分实际需要的内容。使用模块的人不需要知道或接触其内部变量或辅助逻辑,只需要知道其公共接口。
我们来看一个简单的例子:一个计数器模块直接暴露其内部状态,因此任何导入它的内容都可以直接更改该状态,而不是通过适当的函数。
// counter.js
let count = 0;
module.exports = { count };
Enter fullscreen mode Exit fullscreen mode
// app.js
const counter = require('./counter');
counter.count = 100; // 糟糕。没有任何东西阻止这个。
console.log(counter.count); // 100
Enter fullscreen mode Exit fullscreen mode
没有任何东西保护 count 不被从模块外部直接且不正确地更改。
封装版本
// counter.js
let count = 0;
function increment() {
count++;
return count;
}
function getCount() {
return count;
}
module.exports = { increment, getCount };
Enter fullscreen mode Exit fullscreen mode
// app.js
const counter = require('./counter');
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
Enter fullscreen mode Exit fullscreen mode
现在 count 根本无法直接访问。唯一与之交互的方式是通过 increment() 和 getCount(),这些是模块明确选择暴露的函数。
这可以防止内部状态以意外或无效的方式被更改,并且意味着内部实现(count 如何存储或更新)可以在以后更改,而不会破坏依赖于该模块的任何内容,只要 increment() 和 getCount() 的行为保持不变。
3. 防止全局命名空间污染
如果没有模块,你在脚本中声明的每个变量和函数都可能共享同一个全局作用域。如果两个文件(或两个库)恰好使用相同的变量名,其中一个可能会默默地覆盖另一个,导致难以追踪的错误。
例如,想象两个独立的文件都使用一个名为 data 的变量,而没有任何模块系统:
// file1.js
var data = 'user info';
Enter fullscreen mode Exit fullscreen mode
// file2.js
var data = 'product info';
Enter fullscreen mode Exit fullscreen mode
如果这两个文件在同一个全局作用域中运行(比如同一页面上的两个 <script> 标签,或在旧式 Node 设置中一起加载的两个文件),第二个 data 会覆盖第一个。无论哪个文件最后运行都会“获胜”,而且没有任何警告表明出了问题。
Node.js 模块通过为每个文件提供自己的作用域来解决这个问题。在模块内部声明的任何内容,除非明确导出,否则对该文件保持私有:
// file1.js
const data = 'user info';
module.exports = { data };
Enter fullscreen mode Exit fullscreen mode
// file2.js
const data = 'product info';
module.exports = { data };
Enter fullscreen mode Exit fullscreen mode
// app.js
const file1 = require('./file1');
const file2 = require('./file2');
console.log(file1.data); // user info
console.log(file2.data); // product info
Enter fullscreen mode Exit fullscreen mode
两个 data 变量可以安全共存,因为每个模块都有自己独立的作用域。除非你明确导出,否则没有任何内容会泄漏到全局作用域。
4. 提高可维护性和可重用性
一旦代码按职责拆分为模块,两件事自然会变得更容易:维护它和重用它。
可维护性得到改善,因为每个模块都有一个明确的单一目的。如果在价格计算方式中出现错误,你确切地知道去哪里查找——定价模块,而不是扫描整个应用程序。
可重用性得到改善,因为编写良好的模块不关心谁在使用它。回到我们之前的 math.js 示例:该文件可以导入到一个完全不同的项目中,并且工作方式完全相同,因为它不依赖于原始应用程序的任何特定内容。
// Could be reused in any project, unchanged
const { add, subtract, multiply } = require('./math');
Enter fullscreen mode Exit fullscreen mode
这是到目前为止所涵盖的一切的回报:组织代码、封装内部细节和避免全局作用域冲突,所有这些都导致模块更容易单独维护,也更容易跨项目重用。
结论
作为一名前端开发者,我一直认为自己对模块的理解已经足够好了——它们只是文件,对吧?但深入了解它们为什么存在,改变了我对代码结构思考的方式。模块不仅仅是为了组织而拆分文件;它们是关于关注点分离、保护内部状态、避免命名冲突,以及构建更容易维护和长期重用的代码。
这只是触及表面——还有更多内容需要探索,比如 CommonJS 和 ES Modules 之间的差异、Node 中模块缓存的工作原理(无论你 require 它多少次,一个模块只被加载和执行一次),或者 Node 内部如何解析模块路径。但在深入了解如何之前,理解为什么模块重要,已经让其余的学习更快地融入到位。
如果你也在学习 Node.js,我很想听听你对模块的看法,以及对你来说什么理解了(或没理解)。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.