CommonJS 模块化简易实现

news/2024/2/28 16:10:52

在这里插入图片描述

原文出自:https://www.pandashen.com


CommonJS 概述

CommonJS 是一种模块化的标准,而 NodeJS 是这种标准的实现,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

NodeJS 模块化的简易实现

在实现模块加载之前,我们需要清除模块的加载过程:

  • 假设 A 文件夹下有一个 a.js,我们要解析出一个绝对路径来;
  • 我们写的路径可能没有后缀名 .js.json
  • 得到一个真实的加载路径(模块会被缓存)先去缓存中看一下这个文件是否存在,如果存在返回缓存 没有则创建一个模块;
  • 得到对应文件的内容,加一个闭包,把内容塞进去,之后执行即可。

1、提前加载需要用到的模块

因为我们只是实现 CommonJS 的模块加载方法,并不会去实现整个 Node,在这里我们需要依赖一些 Node 的模块,所以我们就 “不要脸” 的使用 Node 自带的 require 方法把模块加载进来。

// 依赖模块
// 操作文件的模块
const fs = require("fs");// 处理路径的模块
const path = require("path");// 虚拟机,帮我们创建一个黑箱执行代码,防止变量污染
const vm = require("vm");

2、创建 Module 构造函数

其实 CommonJS 中引入的每一个模块我们都需要通过 Module 构造函数创建一个实例。

// 创建 Module 构造函数
/*
* @param {String} p
*/
function Module(p) {this.id = p; // 当前文件的表示(绝对路径)this.exports = {}; // 每个模块都有一个 exports 属性,用来存储模块的内容this.loaded = false; // 标记是否被加载过
}

3、定义静态属性存储我们需要使用的一些值

// Module 静态变量
// 函数后面需要使用的闭包的字符串
Module.wrapper = ["(function (exports, require, module, __dirname, __filename) {","\n})"
];// 根据绝对路径进行缓存的模块的对象
Module._cacheModule = {};// 处理不同文件后缀名的方法
Module._extensions = {".js": function() {},".json": function() {}
};

4、创建引入模块的 req 方法

为了防止和 Node 自带的 require 方法重名,我们将模拟的方法重命名为 req

// 引入模块方法 req
/*
* @param {String} moduleId
*/
function req(moduleId) {// 将 req 传入的参数处理成绝对路径let p = Module._resolveFileName(moduleId);// 生成一个新的模块let module = new Module(p);
}

在上面代码中,我们先把传入的参数通过 Module._resolveFileName 处理成了一个绝对路径,并创建模块实例把绝对路径作为参数传入,我们现在实现一下 Module._resolveFileName 方法。

5、返回文件绝对路径 Module._resolveFileName 方法的实现

这个方法的功能就是将 req 方法的参数根据是否有后缀名两种方式处理成带后缀名的文件绝对路径,如果 req 的参数没有后缀名,会去按照 Module._extensions 的键的后缀名顺序进行查找文件,直到找到后缀名对应文件的绝对路径,优先 .js,然后是 .json,这里我们只实现这两种文件类型的处理。

// 处理绝对路径 _resolveFileName 方法
/*
* @param {String} moduleId
*/
Module._resolveFileName = function(moduleId) {// 将参数拼接成绝对路径let p = path.resolve(moduleId);// 判断是否含有后缀名if (!/\.\w+$/.test(p)) {// 创建规范规定查找文件后缀名顺序的数组 .js .jsonlet arr = Object.keys(Module._extensions);// 循环查找for (let i = 0; i < arr.length; i++) {// 将绝对路径与后缀名进行拼接let file = p + arr[i];// 查找不到文件时捕获异常try {// 并通过 fs 模块同步查找文件的方法对改路径进行查找,文件未找到会直接进入 catch 语句fs.accessSync(file);// 如果找到文件将该文件绝对路径返回return file;} catch (e) {// 当后缀名循环完毕都没有找到对应文件时,抛出异常if (i >= arr.length) throw new Error("not found module");}}} else {// 有后缀名直接返回该绝对路径return p;}
};

6、加载模块的 load 方法

// 完善 req 方法
/*
* @param {String} moduleId
*/
function req(moduleId) {// 将 req 传入的参数处理成绝对路径let p = Module._resolveFileName(moduleId);// 生成一个新的模块let module = new Module(p);// ********** 下面为新增代码 **********// 加载模块let content = module.load(p);// 将加载后返回的内容赋值给模块实例的 exports 属性上module.exports = content;// 最后返回 模块实例的 exports 属性,即加载模块的内容return module.exports;// ********** 上面为新增代码 **********
}

上面代码实现了一个实例方法 load,传入文件的绝对路径,为模块加载文件的内容,在加载后将值存入模块实例的 exports 属性上最后返回,其实 req 函数返回的就是模块加载回来的内容。

// load 方法
// 模块加载的方法
Module.prototype.load = function(filepath) {// 判断加载的文件是什么后缀名let ext = path.extname(filepath);// 根据不同的后缀名处理文件内容,参数是当前实例let content = Moudule._extensions[ext](this);// 将处理后的结果返回return content;
};

7、实现加载 .js 文件和 .json 文件的方法

还记得前面准备的静态属性中有 Module._extensions 就是用来存储这两个方法的,下面我们来完善这两个方法。

// 处理后缀名方法的 _extensions 对象
Module._extensions = {".js": function(module) {// 读取 js 文件,返回文件的内容let script = fs.readFileSync(module.id, "utf8");// 给 js 文件的内容增加一个闭包环境let fn = Module.wrap(script);// 创建虚拟机,将我们创建的 js 函数执行,将 this 指向模块实例的 exports 属性vm.runInThisContext(fn).call(module.exports,module.exports,req,module);// 返回模块实例上的 exports 属性(即模块的内容)return module.exports;},".json": function(module) {// .json 文件的处理相对简单,将读出的字符串转换成对象即可return JSON.parse(fs.readFileSync(module.id, "utf8"));}
};

我们这里使用了 Module.wrap 方法,代码如下,其实帮助我们加了一个闭包环境(即套了一层函数并传入了我们需要的参数),里面所有的变量都是私有的。

// 创建闭包 wrap 方法
Module.wrap = function(content) {return Module.wrapper[0] + content + Module.wrapper[1];
};

Module.wrapper 的两个值其实就是我们需要在外层包了一个函数的前半段和后半段。

这里我们要划重点了,非常重要:
1、我们在虚拟机中执行构建的闭包函数时利用执行上/下文 callthis 指向了模块实例的 exports 属性上,所以这也是为什么我们用 Node 启动一个 js 文件,打印 this 时,不是全局对象 global,而是一个空对象,这个空对象就是我们的 module.exports,即当前模块实例的 exports 属性。
2、还是第一条的函数执行,我们传入的第一个参数是改变 this 指向,那第二个参数是 module.exports,所以在每个模块导出的时候,使用 module.exports = xxx,其实直接替换了模块实例的值,即直接把模块的内容存放在了模块实例的 exports 属性上,而 req 最后返回的就是我们模块导出的内容。
3、第三个参数之所以传入 req 是因为我们还可能在一个模块中导入其他模块,而 req 会返回其他模块的导出在当前模块使用,这样整个 CommonJS 的规则就这样建立起来了。

8、对加载过的模块进行缓存

我们现在的程序是有问题的,当重复加载了一个已经加载过得模块,当执行 req 方法的时候会发现,又创建了一个新的模块实例,这是不合理的,所以我们下面来实现一下缓存机制。

还记得之前的一个静态属性 Module._cacheModule,它的值是一个空对象,我们会把所有加载过的模块的实例存储到这个对象上。

// 完善 req 方法(处理缓存)
/*
* @param {String} moduleId
*/
function req(moduleId) {// 将 req 传入的参数处理成绝对路径let p = Module._resolveFileName(moduleId);// ********** 下面为新增代码 **********// 判断是否已经加载过if (Module._cacheModule[p]) {// 模块存在,如果有直接把 exports 对象返回即可return Module._cacheModule[p].exprots;}// ********** 上面为新增代码 **********// 生成一个新的模块let module = new Module(p);// 加载模块let content = module.load(p);// ********** 下面为新增代码 **********// 存储时是拿模块的绝对路径作为键与模块内容相对应的Module._cacheModule[p] = module;// 是否缓存表示改为 truemodule.loaded = true;// ********** 上面为新增代码 **********// 将加载后返回的内容赋值给模块实例的 exports 属性上module.exports = content;// 最后返回 模块实例的 exports 属性,即加载模块的内容return module.exports;
}

9、试用 req 加载模块

在同级目录下新建一个文件 a.js,使用 module.exports 随便导出一些内容,在我们实现模块加载的最下方尝试引入并打印内容。

// 导出自定义模块
// a.js
module.exports = "Hello world";
// 检测 req 方法
const a = req("./a");
console.log(a); // Hello world

<hr/>

CommonJS 模块查找规范

其实我们只实现了 CommonJS 规范的一部分,即自定义模块的加载,其实在 CommonJS 的规范当中关于模块查找的规则还有很多,具体的我们就用下面的流程图来表示。

在这里插入图片描述

这篇文章让我们了解了 CommonJS 是什么,主要目的在于理解 Node 模块化的实现思路,想要更深入的了解 CommonJS 的实现细节,建议看一看 NodeJS 源码对应的部分,如果觉得源码比较多,不容易找到模块化实现的代码,也可以在 VSCode 中通过调用 require 方法引入模块时,打断点调试,一步一步的跟进到 Node 源码中查看。


https://www.jiucaihua.cn/news/show-2888663.html

相关文章

提升iOS审核通过率之“IPv6兼容测试”

商业转载请联系腾讯WeTest授权&#xff0c;非商业转载请注明出处。原文链接&#xff1a;http://wetest.qq.com/lab/view/285.html一、背景在WWDC2015大会上苹果宣布iOS9将支持纯IPv6的网络服务。2016年6月1号&#xff0c;所有提交到AppStore上的应用都必须支持IPv6&#xff0c;…

设计模式(二)工厂模式

实现了创建者和调用者的分离 详细分类&#xff1a; 简单工厂模式&#xff1a;用来生产同一等级结构中的任意产品。&#xff08;用于增加新的产品&#xff0c;需要修改已有代码&#xff09; 工厂方法模式&#xff1a;用来生产同一等级结构中的固定产品。&#xff08;支持增加任意…

销售交货过账的问题

销售交货过账的问题 [复制链接]liuliti 论坛徽章: 0 电梯直达 1# 发表于 2014-7-10 10:27 | 只看该作者 1、一张大销售订单&#xff0c;定期分批交货&#xff0c;可以生成一张多行的交货单吗&#xff1f;每次均部分过账。 2、一张交货单&#xff0c;只有一行&#xff0c;可…

js简易函数性能测试器

如果你不想用浏览器的js性能测试工具&#xff0c;可以用下面这个简单的函数测试一下(1毫秒一下的就测不出来了) function testFn(fn,param){var start new Date().getTime();fn(param);var end new Date().getTime();console.log(fn.name"耗时&#xff1a;"(end - …

http访问时出现异常时提示的状态码

为什么80%的码农都做不了架构师&#xff1f;>>> 一、200状态码&#xff1a; •成功2&#xff1a; 成功处理了请求的状态码。 •1、200 &#xff1a;服务器已成功处理了请求并提供了请求的网页。 •2、204&#xff1a; 服务器成功处理了请求&#xff0c;但没有返回任…

IT男剁手指南:花钱也是一门大学问

摘要&#xff1a; 格子衬衫&#xff0c;全框眼镜&#xff0c;Debug&#xff0c;纯粹而低调&#xff0c;自由而率性&#xff0c;这是IT从业者&#xff0c;不同于单调粗糙&#xff0c;IT人们的生活也是多彩明亮的。他们才不是只会买logo衫的没品一族&#xff0c;他们会亲手将款式…

销售订单_贷项借项型 要在文本备注原因

销售订单_贷项借项型 www.fenginfo.com 在销售过程中&#xff0c;如果货物有损坏或缺陷的情况发生&#xff0c;经投诉及协商后不发生退货&#xff0c;但会给予金额补偿&#xff0c;这时可用贷项销售凭证&#xff0c;将金额贷记到客户科目中。这种凭证只对客户结算的金额进行调…

快速回顾C#基础(编程的小技巧)待完善

一、类型转换 二、可变参数 三、字符串 四、委托 五、构造函数 六、Lamba表达式 七、Linq 八、Entity Framework 1.程序的入口&#xff1a;Entities 文件后缀Context.cs 2.数据库的相关信息都在生成的一个与数据库表对应的类中 文件数据库名.tt中的表名&#xff08…

iOS-解决NSTimer强引用

2019独角兽企业重金招聘Python工程师标准>>> 前言 NSTimer的官方文档对于target的解释&#xff0c;The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer…

winScp 显示本地和远程文件 FTP模式窗口

如果习惯了使用FTP工具&#xff0c;WinScp工具界面修改方式&#xff0c;如图&#xff1a; 大小: 32.6 KB 查看图片附件