Node.js 模块化
介绍
模块化与模块
将一个复杂的程序文件依据一定规则(规范)拆分成多个文件的过程称之为模块化。其中拆分的每一个文件就是模块,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用。
- 在 Node.js 中,文件就是模块,每个
.js
或.json
文件都可以被看作是一个独立的模块。 - Node.js 会缓存加载过的模块,这意味着如果同一个模块被多次请求,它只会在第一次加载时执行,后续请求将直接从缓存中读取。
- 每个模块都有自己的局部变量和函数作用域,这有助于防止全局命名空间的污染。
导入与导出
-
导出:使用
module.exports
对象来暴露模块中的函数、对象或值。例如:// myModule.js
function sayHello() {
console.log('Hello!');
}module.exports = {
sayHello: sayHello
};
// or
exports.sayHello = sayHello;
- module.export可以暴露任意数据
- 不能使用exports = value的形式暴露数据(比如exports = '1',打印的结果会是空对象{}),模块内部module与exports的隐式关系 exports = module.exports = {}
举例说明:
// a.js
const b = require('./b');
console.log(b) // 输出{}
// b.js
exports= '1';
console.log(module.exports) // 输出{}
console.log(exports === module.exports) // 输出true
repuire
返回的结果是目标模块的module.exports
的值,并不是exports
的值,因为require
机制设计的本质就是返回模块的导出值,而这个导出值正是由module.exports
确定的。
举例说明
c.js
exports.greeting = 'Hello,would';
// 等于
exports.module.greeting = 'Hello,would';
当你在另一个文件中使用require
加载c.js
时:
const c = require(./c);
console.log(c.greeting) // 输出 "Hello, world!"
上面的例子中,c
变量将指向c.js
文件中module.exports
对象的内容。
可以像图示这样理解:
然而,需要注意的是,如果在模块中直接将module.exports
或exports
重新赋值给一个新的对象或值,这将切断它们之间的默认关联。例如:
// myModule.js
module.exports = { greeting: "Hello, world!" };
// 或者
exports = { greeting: "Hello, world!" };
在上述任何一种情况下,如果使用
exports
进行重新赋值,则module.exports
将不再被更新,反之亦然。因此,在实际开发中,通常推荐直接操作module.exports
以避免意外的副作用。这样可以确保无论何时使用require
,都会获得准确的模块导出值
-
导入:使用
require()
函数来引入其他模块。例如:// app.js
const myModule = require('./myModule');myModule.sayHello();
-
默认导出单个实体
// myModule.js
export default function sayHello() {
console.log('Hello!');
} -
导入
// app.js
import sayHello from './myModule';
sayHello();
Node.js的导入/导出语法默认遵循CommonJS规范 ,对于ES6的import/export语法的支持是在Node.js v14.5.0 及更高版本中添加的,并且需要在文件扩展名或文件顶部指定
type="module"
。
require使用的一些注意事项:
- 自己创建的模块,导入路径时建议写相对路径,且不能省略
./
或../
- js和json文件导入时可以不用写后缀,如果出现文件名相同,则
js
优先 - 如果导入其他类型的文件,会以
js
文件进行处理,如果导入的路径是个文件夹,则会先去检测该文件夹下package.json
文件中的main
属性对应的文件,存在则导入,反之则报错,如果main
属性不存在,或者package.json
不存在,则会尝试导入文件夹在的index
.js和index.json
,如果找不到,则报错。
文件树:
│ main.js
│
└─modules
app.js
package.json
// mian.js
const greeting = require('./modules');
console.log(greeting); // 打印 hello
// app.js
module.exports = 'hello';
// pageage.json
{
"main": "./app.js"
}
- 导入 node.js 内置模块时,直接require模块名即可,无需
../
和./
导入模块的基本流程
介绍require
导入自定义模块的基本流程
// 新建greeting.js
module.exports = 'Hello World!';
-
将相对路径转为绝对路径,定位目标文件
// 引入模块
const greeting = require('./greeting.js');// 伪代码
function require(file) {
// 1. 将相对路径转为绝对路径
const absolutePath = path.resolve(__dirname, file);
}// 引入模块
const greeting = require('./greeting.js'); -
缓存检测
function require(file) {
// 1. 将相对路径转为绝对路径
const absolutePath = path.resolve(__dirname, file);// 2. 缓存检测 if(caches[absolutePath]) { return caches[absolutePath]; }
}
const greeting = require('./greeting.js');
-
读取目标文件代码
function require(file) {
// 1. 将相对路径转为绝对路径
const absolutePath = path.resolve(__dirname, file);// 2. 缓存检测 if(caches[absolutePath]) { return caches[absolutePath]; }; // 3. 读取目标文件代码 readFileAsync返回的是buffer,用toString转换一下 let code = fs.readFileAsync(absolutePath).toString();
}
const greeting = require('./greeting.js');
-
包裹为一个函数并执行(自执行函数)。通过
arguments.callee.toString()
查看自执行函数// 修改greeting.js
const greeting = {
info: 'Hello World!'
}module.exports = greeting;
// 输出 arguments.callee指向函数 toString可以看到函数的代码体
console.log(arguments.callee.toString());
function require(file) {
// 1. 将相对路径转为绝对路径
const absolutePath = path.resolve(__dirname, file);
// 2. 缓存检测
if (caches[absolutePath]) {
return caches[absolutePath];
};
// 3. 读取目标文件代码
let code = fs.readFileAsync(absolutePath).toString();
// 4. 包裹为一个函数并执行(自执行函数)。通过`arguments.callee.toString()`查看自执行函数
const module = {}; // 实参1
const exports = {}; // 实参2
(function (exports, require, module, __filename, __dirname) {
const greeting = {
info: 'Hello World!'
}
module.exports = greeting;
// 输出
console.log(arguments.callee.toString());
})(exports, require, module, __filename, __dirname); // 修改为自执行函数 需要传入实参1,2
}
const greeting = require('./greeting.js');
-
缓存模块的值
function require(file) {
// 1. 将相对路径转为绝对路径
const absolutePath = path.resolve(__dirname, file);// 2. 缓存检测 if (caches[absolutePath]) { return caches[absolutePath]; }; // 3. 读取目标文件代码 let code = fs.readFileAsync(absolutePath).toString(); // 4. 包裹为一个函数并执行(自执行函数)。通过`arguments.callee.toString()`查看自执行函数 const module = {}; // 实参1 const exports = {}; // 实参2 (function (exports, require, module, __filename, __dirname) { const greeting = { info: 'Hello World!' } module.exports = greeting; // 输出 console.log(arguments.callee.toString()); })(exports, require, module, __filename, __dirname); // 修改为自执行函数 需要传入实参1,2 // 5. 缓存结果值 caches[absolutePath] = module.exports;
}
const greeting = require('./greeting.js');
-
返回
module.exports
的值function require(file) {
// 1. 将相对路径转为绝对路径
const absolutePath = path.resolve(__dirname, file);// 2. 缓存检测 if (caches[absolutePath]) { return caches[absolutePath]; }; // 3. 读取目标文件代码 let code = fs.readFileAsync(absolutePath).toString(); // 4. 包裹为一个函数并执行(自执行函数)。通过`arguments.callee.toString()`查看自执行函数 const module = {}; // 实参1 const exports = {}; // 实参2 (function (exports, require, module, __filename, __dirname) { const greeting = { info: 'Hello World!' } module.exports = greeting; // 输出 console.log(arguments.callee.toString()); })(exports, require, module, __filename, __dirname); // 修改为自执行函数 需要传入实参1,2 // 5. 缓存结果值 caches[absolutePath] = module.exports; // 6. 返回 retrun module.exports;
}
const greeting = require('./greeting.js');
模块化项目
编码时是按照一个一个编码,整个项目就是一个模块化的项目
模块路径
- 相对路径: 如果你引用的是项目内的模块,可以使用相对路径。
- 绝对路径: 引用系统模块或安装的第三方模块时,使用绝对路径或模块名称即可。
模块解析
当 Node.js 尝试解析模块路径时,它会遵循以下步骤:
- 检查是否为内置模块。
- 尝试在当前目录下查找模块。
- 检查
node_modules
目录。 - 查找指定的
.js
,.json
, 或.node
文件。
模块化优点
- 防止命名冲突
- 高复用性
- 高维护性
CommonJS规范
module.exports
、exports
以及require
这些都属于CommonJS
模块化规范中的内容,而Node.js
是实现了CommonJS
模块化规范,二者关系有点像JavaScript
与ECMASriprt
的关系。