什么是模块化?深析node模块化的那些事

1. 什么是模块化?

模块化是指将一个大的程序拆分成小的模块,使得每个模块只负责实现某个特定的功能。这样做的好处有以下几点:

提高代码的可重用性:模块化可以让开发者将一些常用的功能封装成一个独立的模块,减少重复代码的编写,提高开发效率。

提高代码的可维护性:模块化可以让代码结构更加清晰,模块之间的关系更加明了,修改某一个模块时不用担心影响到其他模块。

提高代码的安全性:模块化可以让一些敏感的代码如密码等信息只在模块内部进行使用,对于外部来说是不能访问到的,从而提高了代码的安全性。

2. node模块化的那些事

2.1 CommonJS规范

在node中,使用的是CommonJS规范来进行模块化开发的。该规范定义了模块的基本结构和模块的引用方式。一个模块可以通过module.exports对象来向外部导出接口,其他模块可以通过require函数来引入模块。

// 导出模块

module.exports = {

sayHello: function() {

console.log('hello');

},

sayHi: function() {

console.log('hi');

}

};

// 引入模块

const myModule = require('./myModule');

myModule.sayHello();

在以上代码中,我们通过module.exports对象将两个函数导出,其他模块可以通过require函数来引入myModule模块。

2.2 模块的查找规则

我们在通过require函数引入模块时,需要注意到模块的查找规则。其实这个规则很简单,node会沿着路径一级一级往上查找,直到找到模块为止。以下是node模块查找的顺序:

内置模块(例如fs、http等)

当前目录下的模块(例如./myModule)

上级目录下的模块(例如../myModule)

node_modules目录下的模块

其中第4步比较特殊,当需要引用的模块不在当前目录下或者任何上级目录下时,node会在当前目录下的node_modules目录中查找。需要注意的是,node会一层层往上找node_modules目录,直到找到根目录为止。

// 导入lodash模块

const _ = require('lodash');

在以上代码中,我们通过require函数来引入了lodash模块,node会先在内置模块中查找,发现没有该模块,然后会在当前目录下的node_modules目录中查找,如果还是没有找到则会往上一层目录里找,以此类推。

2.3 模块的缓存机制

在node中,每个模块都有一个缓存对象,当我们通过require函数来加载模块时,node会先检查缓存对象中是否已经有该模块,如果有,则直接返回缓存中的模块,如果没有,则会加载该模块并将其添加到缓存中。

需要注意的是,当我们在require函数中传入的是相对路径时,node会将该路径解析为绝对路径,并保存到缓存对象中。当我们再次使用相对路径引入时,node会直接使用缓存对象中保存的绝对路径来查找模块,而不是再次解析相对路径。

以下是一个示例代码,演示了模块的缓存机制:

// counter.js

let count = 0;

module.exports = function() {

return ++count;

}

// main.js

const counter1 = require('./counter');

console.log(counter1()); // 输出 1

console.log(counter1()); // 输出 2

const counter2 = require('./counter');

console.log(counter2()); // 输出 3

console.log(counter2()); // 输出 4

在以上代码中,我们定义一个counter模块,模块中有一个计数器,每次调用导出的函数会返回自增的计数器值。当我们第一次引入counter模块时,node会调用模块中的代码并将其指定到缓存对象中,第二次引入时,node会直接从缓存中取出该模块。我们可以看到,每个计数器的值都是正确递增的。

2.4 模块循环引用

在node模块化中,循环引用是一个很常见的问题。简单来说,循环引用就是两个或多个模块之间互相引用导致的死循环。例如下面的代码:

// a.js

const b = require('./b');

console.log('a.js 中 b.foo 的值为:', b.foo);

module.exports.a = 'a';

// b.js

const a = require('./a');

console.log('b.js 中 a.a 的值为:', a.a);

module.exports.foo = 'foo';

在以上代码中,a.js引入了b.js,而b.js又引入了a.js。当我们尝试运行a.js时会发现node会进入无限循环,最终内存溢出。为什么会出现这种情况呢?其实很简单,因为node先加载了a.js,又加载了b.js,但是又需要加载a.js,于是就形成了死循环。

为了解决循环引用问题,node采用的方法是将模块中exports对象的指针赋值为module.exports对象,这样可以保证两个指针都指向同一个对象,避免了循环引用时的死循环。

// a.js

const b = require('./b');

console.log('a.js 中 b.bar 的值为:', b.bar);

exports.a = 'a';

// b.js

const a = require('./a');

console.log('b.js 中 a.a 的值为:', a.a);

exports.bar = 'bar';

在以上代码中,我们采用exports对象来导出a模块和b模块的接口,避免了循环引用时的死循环。

2.5 ES6模块化

除了CommonJS规范之外,ES6也提供了一套模块化机制。与CommonJS不同的是,ES6模块化是静态的,导入和导出的只能是变量,不能是代码块或语句。以下是一个简单的示例:

// counter.js

let count = 0;

export function increment() {

return ++count;

}

// main.js

import { increment } from './counter';

console.log(increment()); // 输出 1

console.log(increment()); // 输出 2

在以上代码中,我们使用ES6的模块化机制来实现了一个计数器。在counter.js中,我们使用export关键字导出increment函数。在main.js中,我们使用import关键字来导入increment函数,并调用两次increment函数来测试其是否可以正常工作。

2.6 node中的ES6模块化

在node中,使用ES6的模块化需要在文件后缀名中加上.mjs。例如我们有一个test.mjs文件,可以通过以下方式来使用它:

// 引入ES6模块

import { foo } from './test.mjs';

// 打印 foo

console.log(foo);

需要注意的是,当我们在ES6模块中使用require函数引入CommonJS模块时,需要使用到interop模块。例如:

// 引入外部CommonJS模块

const moment = require('moment');

// 将moment转成ES6模块

const momentES6 = interopDefault(moment);

export default momentES6;

在以上代码中,我们通过interop模块将moment转成ES6模块,并使用export default关键字将其导出,以便其他ES6模块可以直接引入并使用。

总结

模块化是现代开发中不可或缺的一部分,通过将大的程序拆分成小的模块可以提高代码的可重用性、可维护性和安全性。在node中,我们使用CommonJS规范来实现模块化,并使用require函数来引入模块。在ES6中,我们使用静态的导入和导出语法来实现模块化。需要注意的是,在node中使用ES6模块化需要在文件后缀名中加上.mjs,并且在ES6模块中使用到CommonJS模块时,需要使用到interop模块。

免责声明:本文来自互联网,本站所有信息(包括但不限于文字、视频、音频、数据及图表),不保证该信息的准确性、真实性、完整性、有效性、及时性、原创性等,版权归属于原作者,如无意侵犯媒体或个人知识产权,请来电或致函告之,本站将在第一时间处理。猿码集站发布此文目的在于促进信息交流,此文观点与本站立场无关,不承担任何责任。