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模块。