nodejs中exports与module.exports的关系以及手写require导入模块的原理

在这里插入图片描述

一、模块化

1.1 什么是模块化?

将一个复杂的程序文件依据一定规则(规范),拆分成多个文件的过程称之为 模块化
其中拆分出的 每一个文件就是一个模块,模块内部数据是私有的,不过模块可以暴露内部数据以供其他模块使用。

1.2 模块的分类

  1. 内置模块
    如 nodejs 中的 fs、path、http 等模块,加载时无需具体路径:const fs = require('fs');
  2. 第三方模块
    通过包管理器(npm、yarn、cnpm)安装的模块,如 const dayjs = require('dayjs');
  3. 自定义模块
    就是自己项目中拆分的文件,如vue项目中的单独文件 require('./utils/request.js');

1.3 模块化好处

  • 防止命名冲突
  • 高复用性
  • 高维护性

二、require 以及 exports、module.exports 的注意点

1.1 对 require 的理解

require 加载模块可以分为两类,判断区别是 require() 参数中是否含有具体路径:

  1. 加载模块(内置模块、第三方模块):require('fs');
  2. 加载文件(自定义模块):require('./utils/request.js');

require 使用时的注意点

  1. 对于自定义模块,导入时路径建议写相对路径,且不能省略 ./../
  2. js 和 json 文件导入时可以不用写后缀;
  3. 如果导入其他类型的文件,会以 js 文件进行处理;
  4. 如果导入的路径是个文件夹,则首先检测该文件夹下 package.json 文件中 main 属性对应的文件,如果main属性不存在,或者package.json 不存在,则会检测文件夹下的 index.js 和 index.json,如果还是找不到,则报错;
  5. 导入 node.js内置模块时,直接 require 模块的名称即可,无需添加 ./../

exports、module.exports 以及 require 这些都是 CommonJS 模块化规范中的内容,而 Node.js 实现了 CommonJS 模块化规范

参考 Node.js 官方文档:CommonJS 模块

rquire在加载模块时,模块在第一次加载后被缓存。当前文件再次加载模块时从缓存(require.cache)中获取。

1.2 exports与module.exports

node中有两种导出模块的方式:

  1. module.exports = value;
  2. exports.name = value;
const test = {
    
     a: 111 };
// 第一种导出方式
module.exports = test;
// 第二种导出方式
exports.test = test;

注意点

  • 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 模块封装器:

当模块被加载的时候,会被包裹在一个函数内,这个包裹函数就是 模块封装器

CommonJS 模块 | 模块封装器

在这里插入图片描述
在代码中查看当前 模块封装器

// a.js
let test = {
    
    
	a: 111
}
module.exports = test;
console.log(arguments.callee.toString()); // 输出封装器函数的函数体

在这里插入图片描述

1.2 手写 require 函数:

介绍一下 require 导入 自定义模块 的基本流程:

  1. 将相对路径转为绝对路径,定位目标文件;
  2. 缓存检测;
  3. 读取目标文件代码;
  4. 包裹一个函数并执行(自执行函数)。通过 arguments.callee.toString() 查看自执行函数;
  5. 缓存模块的值;
  6. 返回 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 

猜你喜欢

转载自blog.csdn.net/ThisEqualThis/article/details/129840839