文章目录
一、模块化
1.1 什么是模块化?
将一个复杂的程序文件依据一定规则(规范),拆分成多个文件的过程称之为 模块化
。
其中拆分出的 每一个文件就是一个模块,模块内部数据是私有的,不过模块可以暴露内部数据以供其他模块使用。
1.2 模块的分类
- 内置模块
如 nodejs 中的 fs、path、http 等模块,加载时无需具体路径:const fs = require('fs');
- 第三方模块
通过包管理器(npm、yarn、cnpm)安装的模块,如const dayjs = require('dayjs');
- 自定义模块
就是自己项目中拆分的文件,如vue项目中的单独文件require('./utils/request.js');
1.3 模块化好处
- 防止命名冲突
- 高复用性
- 高维护性
二、require 以及 exports、module.exports 的注意点
1.1 对 require 的理解
require 加载模块可以分为两类,判断区别是 require() 参数中是否含有具体路径:
- 加载模块(内置模块、第三方模块):
require('fs');
- 加载文件(自定义模块):
require('./utils/request.js');
require 使用时的注意点:
- 对于自定义模块,导入时路径建议写相对路径,且不能省略
./
和../
; - js 和 json 文件导入时可以不用写后缀;
- 如果导入其他类型的文件,会以 js 文件进行处理;
- 如果导入的路径是个文件夹,则首先检测该文件夹下 package.json 文件中 main 属性对应的文件,如果main属性不存在,或者package.json 不存在,则会检测文件夹下的 index.js 和 index.json,如果还是找不到,则报错;
- 导入 node.js内置模块时,直接 require 模块的名称即可,无需添加
./
和../
;
exports、module.exports 以及 require 这些都是 CommonJS 模块化规范中的内容,而 Node.js 实现了 CommonJS 模块化规范。
参考 Node.js 官方文档:CommonJS 模块
rquire在加载模块时,模块在第一次加载后被缓存。当前文件再次加载模块时从缓存(require.cache)中获取。
1.2 exports与module.exports
node中有两种导出模块的方式:
- module.exports = value;
- exports.name = value;
const test = {
a: 111 };
// 第一种导出方式
module.exports = test;
// 第二种导出方式
exports.test = test;
注意点:
扫描二维码关注公众号,回复:
17210195 查看本文章
- module.exports 可以导出任意数据;
- 不能使用 exports = name 的形式暴露数据,因为模块内部 exports 与 module.exports 的关系为
exports = module.exports = {};
使用 require 导入数据时,其实导入的是 module.exports 导出的数据。
// a.js
exports = "使用exports导出数据";
// index.js
const data = rquire("./a.js"); // 这里导入的其实是 module.exports,默认值是一个空对象
console.log(data); // {}
// a.js
module.exports = "使用module.exports导出数据";
// index.js
const data = rquire("./a.js");
console.log(data); // 使用module.exports导出数据
三、手写 require 导入自定义模块的代码原理
1.1 模块封装器:
当模块被加载的时候,会被包裹在一个函数内,这个包裹函数就是 模块封装器
。
在代码中查看当前 模块封装器
:
// a.js
let test = {
a: 111
}
module.exports = test;
console.log(arguments.callee.toString()); // 输出封装器函数的函数体
1.2 手写 require 函数:
介绍一下 require 导入 自定义模块
的基本流程:
- 将相对路径转为绝对路径,定位目标文件;
- 缓存检测;
- 读取目标文件代码;
- 包裹一个函数并执行(自执行函数)。通过
arguments.callee.toString()
查看自执行函数; - 缓存模块的值;
- 返回
module.exports
的值。
// a.js
let test = {
a: 111
}
console.log("我是a.js文件");
module.exports = test;
// index.js 封装require函数
const fs = require("fs");
const path = require("path");
_require.cache = {
};
function _require(file) {
// 1. 将相对路径转为绝对路径,定位目标文件;
let absolutePath = path.resolve(__dirname, file);
// 2. 缓存检测;
if (this.cache[absolutePath]) {
return this.cache[absolutePath]; // 被加载过一次,就不再往下执行
};
// 3. 读取目标文件代码;
let fileCode = fs.readFileSync(absolutePath).toString();
let module = {
};
let exports = module.exports = {
};
// 4. 包裹一个函数并执行(自执行函数),这里就是模块封装器;
(function (exports, require, module, __filename, __dirname) {
eval(fileCode); // 因为fileCode是被读取的字符串,所以使用eval()加载
})(exports, _require, module, __filename, __dirname);
// 5. 缓存模块的值;
this.cache[absolutePath] = module.exports;
// 6. 返回 `module.exports` 的值。
return module.exports;
}
// 第一次模块加载时导出的内容被缓存在_require.cache对象中,所以再次导入a.js文件时代码不会被执行,也就不会打印内容了
_require.call(_require, "./a.js"); // 打印:我是a.js文件
_require.call(_require, "./a.js"); // a.js文件代码不会执行
_require.call(_require, "./a.js"); // a.js文件代码不会执行
_require.call(_require, "./a.js"); // a.js文件代码不会执行
let test = _require.call(_require, "./a.js"); // a.js文件代码不会执行
console.log(test.a); // 111