为什么要有模块化?
在早期JavaScript
的开发当中,在没有模块化的情况下。写法是这样的:
<script src="./index.js"></script>
<script src="./home.js"></script>
<script src="./user.js"></script>
这种写法很容易存在全局污染和依赖管理混乱问题。在多人开发前端应用的情况下问题更加明显。
命名混乱、代码组织性低、可维护性差、可重用性低等问题暴露的更加明显。
例如:
命名冲突
在没有模块化的情况下,所有的函数和变量都定义在全局作用域中。这意味着如果不小心命名冲突,不同部分的代码可能会意外地互相影响,导致难以察觉的 bug
或不可预见的行为。
// 文件 1
function calculateTotal(price, quantity) {
return price * quantity;
}
// 文件 2
function calculateTotal(price, taxRate) {
return price * (1 + taxRate);
}
如果这两个文件都在全局作用域中定义,且被同一个 HTML 文件引用,那么 calculateTotal
函数会产生冲突,调用时可能会得到不正确的结果。
全局污染
在没有模块化的情况下,所有的变量和函数都被添加到全局命名空间中。这可能导致变量名重复、不必要的全局变量增多,从而增加了代码的复杂性和维护难度。
// 文件 1
var username = 'Alice';
function greetUser() {
console.log('Hello, ' + username + '!');
}
// 文件 2
var username = 'Bob';
function displayUsername() {
console.log('Current user: ' + username);
}
如果这两个文件都在同一个页面中执行,它们共享同一个全局命名空间,可能会造成 username
被覆盖,从而导致 greetUser
和 displayUsername
函数不再使用预期的 username
值。
难以管理和维护
没有模块化的代码通常难以分离、重用和测试。整体项目结构可能变得混乱,不同功能之间的依赖关系也不明确,增加了代码的复杂性和理解难度,特别是在大型项目中。
示例:
// 文件 1
function calculateTotal(price, quantity) {
return price * quantity;
}
function formatCurrency(amount) {
return '$' + amount.toFixed(2);
}
// 文件 2
function calculateTax(total, taxRate) {
return total * taxRate;
}
function formatCurrency(amount) {
return '¥' + amount.toFixed(2);
}
在没有模块化的情况下,两个文件都在全局作用域中定义 formatCurrency
函数,如果它们都被加载到同一个页面中,会出现函数覆盖和不一致的行为。
模块的概念及使用原因
使用模块化工具(如 ES6 的模块化或 CommonJS)可以有效地解决上述问题。模块化工具允许我们将代码组织成独立的、封闭的模块,每个模块有自己的作用域,只暴露需要的接口,从而避免命名冲突、全局污染和代码管理上的困难。
模块化开发是我们开发当中用于组织和管理代码的方法,它的目的是将复杂的应用程序去拆分为更小和更好管理的模块单元,从而提高代码的复用性和可维护性。
在JavaScript
中,模块化是一种将代码分割成独立、可复用的部分的方法。ES6
引入了ES Modules(ESM)
作为原生的模块系统,而CommonJS
是Node.js
中使用的模块系统。
ES模块
和CommonJS
模块化方案都被广泛使用。以下是两者的详细解释和示例代码。
ES Module
ES Module 是 ECMAScript 6 引入的官方模块化方案,它具有以下特点:
- 使用
import
和export
关键字定义模块。 - 支持静态导入(在编译时解析)和动态导入(在运行时异步加载)。
- 原生支持异步加载,使用
import()
函数。
示例 ES Module:
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.js
import {
add, subtract } from './math';
console.log(add(5, 3)); // 输出 8
console.log(subtract(5, 3)); // 输出 2
自定义模块原则
自定义模块在 Node.js 环境中的查找原则如下:
- 在当前目录下的 node_modules 目录查找。
- 向上级目录逐级查找,直至根目录。
- 查找 package.json 中的 main 属性指定的入口文件。
- 默认文件名查找(index.js、index.json、index.node)。
以下是一个简单的自定义模块查找流程示意图:
在这个示例中,如果要加载模块A,Node.js 会首先在当前目录下的 node_modules
中找到对应的模块文件或者 package.json
中指定的入口文件。如果未找到,则向上逐级查找,直至根目录。
主要特点
- ES Module 是现代 JavaScript 的官方模块化方案,具有静态导入和动态导入的能力,适合在浏览器和 Node.js 环境中使用。
- 自定义模块原则查找流程 确保了在 Node.js 中引入模块时的灵活性和便捷性,无需手动指定路径。
这些特性和原则使得 JavaScript 开发中的模块化更加高效和易于管理。
CommonJS
在ES Module
中,使用export
或import
关键词来导出或导入模块。在CommonJS
中,使用module.exports
或require()
来导出模块和引入模块。两者都是JavaScript
模块化的方式,但是主要应用环境和语法有所区别。在浏览器端,可以通过Webpack
等工具将CommonJS
代码转换为ES Module代码
以便在浏览器中使用。在Node.js
环境中,直接使用CommonJS
模块系统即可。
// math.js
function sum(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
sum, multiply };
// main.js
const {
sum, multiply } = require('./math.js');
console.log(sum(1, 2)); // 输出:3
console.log(multiply(1, 2)); // 输出:2
两者的主要区别在于导出和导入的语法以及在不同环境下的加载机制。ES模块
采用import
和export
语法,而CommonJS
模块采用require
和module.exports
。此外,ES模块
是静态的,需要构建工具转换后才能在不支持ES模块
的环境中运行,而CommonJS模块
可以直接在Node.js
等环境中运行。
主要特点和使用方法:
模块定义和导出:
- 使用
module.exports
导出模块的功能或变量。 - 使用
require()
函数引入其他模块的功能或变量。
- 使用
模块加载的同步性:
- CommonJS 加载模块是同步进行的,即
require()
函数会阻塞代码执行直到模块加载完成。这种同步加载模式在服务器端常用,但在浏览器端可能会影响性能。
- CommonJS 加载模块是同步进行的,即
模块的缓存:
require()
函数加载的模块会被缓存,避免多次加载相同模块时造成的性能损失和状态问题。
动态加载:
require()
的参数可以是动态计算的表达式,允许根据需要动态加载模块,这在某些场景下非常有用。
示例:
const moduleName = './math'; const math = require(moduleName);
适用范围:
- CommonJS 最初设计用于 Node.js 等服务器端 JavaScript 环境,支持在同步 I/O 操作下方便地组织和加载模块。在浏览器端,可以使用工具如 Browserify 或 Webpack 将 CommonJS 格式的模块转换成适合浏览器运行的代码。
优点:
- 简单直观: 使用
module.exports
和require()
的方式非常直观,易于理解和使用。 - 适用于服务器端: 在服务器端 JavaScript 开发中得到广泛应用,因其简单性和实用性。
缺点:
- 同步加载: 阻塞式加载模块可能在大型应用中导致性能问题,特别是在需要异步加载的场景下。
- 浏览器兼容性问题: 浏览器环境并不原生支持 CommonJS,需要使用工具转换或者使用 ECMAScript 模块化规范(ES6 模块)。
自定义模块查找流程
当处理自定义模块时,查找流程通常遵循以下步骤:
在当前目录下的 node_modules 目录查找:
- 首先,Node.js 尝试在当前执行脚本所在的目录中的
node_modules
文件夹中查找需要引入的模块。
- 首先,Node.js 尝试在当前执行脚本所在的目录中的
向上级目录逐级查找:
- 如果在当前目录下未找到,Node.js 将向上级目录逐级查找,直到根目录。每一级目录都会检查其下的
node_modules
文件夹。
- 如果在当前目录下未找到,Node.js 将向上级目录逐级查找,直到根目录。每一级目录都会检查其下的
查找 package.json 中的 main 属性:
- 如果找到模块所在的文件夹,并且该文件夹中包含一个
package.json
文件,Node.js 将查看package.json
文件中的main
属性指定的入口文件。
- 如果找到模块所在的文件夹,并且该文件夹中包含一个
默认文件名查找:
- 如果没有找到
package.json
或者main
属性未定义,Node.js 将默认使用以下文件名来查找入口文件:index.js
index.json
index.node
- 如果没有找到
递归向上直至根目录:
- 如果在当前执行脚本的根目录下的
node_modules
文件夹中仍未找到,Node.js 将放弃查找并抛出一个错误。
- 如果在当前执行脚本的根目录下的
这种查找模块的方式保证了在 Node.js 环境中可以方便地引入自定义模块,而不需要显式指定绝对路径。以下是一个简单的流程图示例:
您好,我是肥晨。
欢迎关注我获取前端学习资源,日常分享技术变革,生存法则;行业内幕,洞察先机。