Skip to content

JavaScript的思想是一切皆对象,而Node.js编程是基于模块化思想的。

1. 基本概念 ​

Node.js采用了CommonJS模块系统,每个文件都是一个独立的模块,每个模块都有自己的作用域,可以有自己的变量和函数。这种模块化的设计使得代码更易于组织、维护和重用。

通过模块化,开发者可以将复杂的程序拆分成小的、独立的模块,每个模块专注于特定的功能。这样做有助于降低代码的耦合度,提高代码的可读性和可维护性。

此外,Node.js的模块化设计还支持模块之间的依赖管理,使得开发者可以方便地引入其他模块提供的功能。

2. 模块实现 ​

模块代码默认是私有的,不会污染全局作用域

使用module.exports 或者 exports 导出模块中变量、函数或对象

示例:

js
// hello.js

// 定义一个函数,用于输出"Hello, World!"
function sayHello() {
    console.log("Hello, World!");
}

// 将函数导出,以便其他文件可以使用
module.exports = sayHello

上面代码定义了 sayHello 函数,并通过 module.exports 暴露接口,使得其它文件可以引入并调用

js
// app.js

const hello = require('./hello')

hello()
// Hello, World!

使用时使用require引入模块,并赋值给 hello,此时 hello = sayHello

3. 共享引用机制 ​

Node.js模块导出的是对象的引用,而不是对象实例,其它模块引入后,实际获取的是同一个对象的引用,这意味着任何修改都会影响到引入该对象的其它模块

js
// module.js

const info = {
    name: 'Gavin',
    sex: 1,
}
module.exports = info
js
// app.js

const infoA = require('./module')
const infoB = require('./module')
console.log('A', infoA.name)
// Gavin

infoA.name = 'Cat'
console.log('A', infoA.name)
// Cat

console.log('B', infoB.name)
// Cat

打印结果可以看出 infoA和infoB两个对象引用的是同一个对象,Node.js这种共享引用机制,一是可以方便模块之间数据共享,二是有利于节省内存

如果导出的是字符串或数值,则不是引用,而是复制了副本

js
// module.js
let name = 'Gavin'
let age = 18

module.exports = {
    name,
    age,
}
js
// app.js
let { name, age } = require('./module')
let { age: age2 } = require('./hello')
age = 20

console.log(age)
// 20

console.log(age2)
// 18

4. 路径解析 ​

  1. 绝对路径解析 当给require()函数传递以斜杆 / 开头的路径,node.js会解析为绝对路径,即相对于文件系统根目录,比如require('/path/demo/module')
  2. 相对路径解析 当给require()函数传递以斜杆 ./或../ 开头的路径,node.js会解析为相对路径,即相对于当前模块文件的路径,比如require('./module')
  3. 核心模块解析 当给require()函数传递不是绝对路径也不是相对路径时,node.js会解析成核心模块,比如require('http')
  4. 模块路径解析 当在本层node_modules目录查找不到核心模块,node.js会逐级往上查找node_modules目录下是否有匹配模块
  5. 文件扩展名解析 当给require()传递 路径不带扩展名,node.js会尝试添加扩展名匹配具体文件,顺序为.js > .json > .node,如果还找不到文件,则抛出MODULE_NOT_FOUND异常

5. 模块缓存 ​

  1. 缓存机制 node.js中为了减少多次加载相同模块的性能开销,会缓存已加载的模块。 当加载模块时,会先检查缓存,已存在则直接换回缓存中的模块,没有才会重新加载,并进行缓存
  2. 缓存清除 加载模块的缓存,存储到require.cache对象,键值为模块绝对路径,通过delete来删除 例如delete require.cache[require.resolve('./hello')]
  3. 缓存更新 对于已经缓存的模块,如果模块文件有更新,缓存并不会更新,只有当再次调用require()加载时,缓存才会更新

6. 循环依赖 ​

循环依赖通常是开发中需要避免的,因为它会增加代码的复杂性,并导致难以维护的代码。

Node.js 模块的加载是同步的,因此循环依赖会导致其中一个模块在加载过程中尝试去加载另一个模块,从而导致死锁或加载错误。

Node.js模块系统检测到循环依赖的情况后,为了避免无限递归,加载中的模块会先返回不完整的exports对象,仅包含已经执行的部分模块内容,未执行的部分在后面继续执行。

通常需要将相互依赖的模块,拆分成更小的模块,以消除循环依赖,另一种方法是使用延迟加载。

循环依赖在《Node.js设计模式》有详细解释,有兴趣可以看看。

7. module.exports 与 exports ​

module对象是Node.js里一个全局对象,表示当前模块本身,每个模块的module对象是独立的,包含了相关信息和属性,以及导出内容的 exports 对象

大部分情况下,exports === module.exports,因为指向同一个导出对象,只有在给 exports 赋值时,此时 exports 会指向新赋予的值,而不再指向 module.exports,所以此时 exports !== module.exports

总的来说,CommonJS 模块规范允许开发者自由选择使用 module.exports 或 exports 导出函数,但需要注意它们之间的微妙差异,以避免出现意外的行为。

8. ES模块 ​

ES模块相对CommonJS模块,主要差异有几点

  1. 语法差异 ES模块使用 export 和 import 进行模块的导出和引入 CommonJS模块使用 module.exports 和 require() 导出和引入
  2. 作用域 ES模块中变量是私有的,不可被全局作用域访问 CommonJS模块的变量是公共的,引用该模块的对象都可访问
  3. 加载方式 ES模块是静态加载,编译时依赖关系就确定了 CommonJS模块是动态加载,依赖关系在运行时才确定
  4. 循环依赖 ES模块不允许循环依赖,会报异常 CommonJS模块运行循环依赖,但不推荐,有可能出现问题,需要谨慎使用
  5. 顶层对象 ES模块没有全局对象,或者说全局对象是undefined,模块变量不会污染全局作用域 CommonJS模块全局对象是global,任何模块中都可以访问到和修改

ES模块代码示例

js
// 导出
export function filterArea(code) {
    // do something
}

// 引入
import { filterArea } from './tools'

9. 总结 ​

在实际开发中,合理利用 Node.js 的模块化机制可以极大地提高项目的开发效率和代码质量。我们可以将不同功能的代码组织成独立的模块,并通过模块之间的依赖管理,实现模块间的数据共享和功能复用。同时,我们还需要注意避免循环依赖等问题,保证模块之间的稳健性和可靠性。

深入理解 Node.js 模块化不仅仅是一种编程技巧,更是一种编程思维方式。只有通过不断地学习、实践和总结,才能更好地掌握 Node.js 模块化的精髓,提升自己的开发能力。

希望本文能够帮助读者更深入地理解 Node.js 模块化,并在实际开发中得到应用。愿大家在 Node.js 开发的路上越走越远,越走越好!

上次更新于: