如何实现|如何实现 node module 模块导入
今天,我们来聊聊 node 的模块,主要内容分别有:
1.什么是模块化?模块化都有哪些规范?
2.node 模块导入具体是如何实现的?模块化历史 单例模式
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 实现单例核心思想 用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
- 单例模式抽象,分离创建对象的函数和判断对象是否已经创建
var getSingle = function (fn) {
var result;
return function () {
return result || ( result = fn.apply(this, arguments) );
}
};
复制代码
形参 fn 是我们的构造函数,我们只要传入任何自己需要的构造函数,就能生成一个新的惰性单例。闭包
- 函数执行后返回一个引用空间这个空间被外部引用,此空间无法销毁。这就叫闭包 函数执行的时候也会产生一个闭包
- 异步模块定义
- 并非JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。
- 1、多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
- 2、js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。
- 通用模块定义
- CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。
- 1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
- 2、CMD推崇就近依赖,只有在用到某个模块的时候再去require。
- 1.模块实现(一个 js 文件就是一个模块)为了实现模块化的功能每个文件外面都包含一个闭包。
- 2.规定如何导出一个模块 module.exports。
- 3.如果导入一个模块 require。
node 导入模块的方式:
const fs = require("fs");
复制代码
我们通过这个入口,一步步的看下 node 是如何实现模块的导入的,断点调试,走起!首先我们看到,在 node 核心文件 module.js 中,定义了一个 Module 类,并且在类的原型上定义了一个 require 方法,而这个方法就是在给定的文件路径下加载模块,并且返回该模块的"exports"对象。
而 require 调用了 Module 类上的静态方法 _load,那我们进去看看这个 _load 方法是如何实现的吧:
【如何实现|如何实现 node module 模块导入】很明显,这里就node module导入的核心代码,那么这里都做了些什么事情呢,我们一一来分析:
- 1.如果缓存中已经存在了将要导入的模块,则直接返回其"exports"对象。
- 2.如果这个模块是原生的模块,那么则调用NativeModule.require()来返回对应的结果。
- 3.如果以上2种都没有,则执行以下操作:
- 1.根据文件名,解析出文件的绝对路径,其对应的是这个操作:
- 2.如果缓存中已经有这个模块,则直接从缓存中获取并返回"exports"对象:
- 3.如果缓存中没有,则文件的绝对路径创建一个模块,并且将这个模块放入到缓存中:
- 4.最后读取文件模块中的内容,将内容放置在"exports"对象上并最终返回"exports"对象:
- 1.根据文件名,解析出文件的绝对路径,其对应的是这个操作:
- Moudle 是一个类。那我们来看看,node里面的Module类都有哪些属性呢?
- 要实现一个 Module._load 方法实现模块的加载。
- Module._resolveFilename 方法是用来解析文件,获取文件的绝对路径。
- Module._cache 模块缓存
- 创建一个模块,放到缓存中
- Module._extensions 不同文件类型解析方式不同,这点我们在实现中细细讲解
首先,我们要读取文件并解析js文件,所以需要使用node底层一些方法:
const fs = require("fs");
const path = require("path");
const vm = require("vm");
// 定义Module类
function Module(file) {
this.id = file;
//当前模块标识
this.exports = {};
//模块必有属性,模块导出时属性挂载在该对象上
}
复制代码
接下来的第二步, Module类中的静态方法Module._load:
function req(moduleId) {
let p = Module._resolveFileName(moduleId);
// 判断缓存中是否已经存在该模块,如果存在则直接返回模块的"exports"对象
if (Module._cacheModule[p]) {
return Module._cacheModule[p].exports;
}
// 缓存中没有加载过这个模块,则构建一个模块
let module = new Module(p);
// 加载模块
let content = module.load(p);
// 将创建出来的模块放入到缓存中,下次调用时直接从缓存中获取
Module._cacheModule[p] = module;
module.exports = content;
// 最后返回模块的exports对象
return module.exports;
}
复制代码
第三步: 我们需要解析文件的具体路径,让我们一起看看 Module._resolveFileName 方法的实现吧:
// 解析绝对路径的方法,返回一个绝对路径
Module._resolveFileName = function (moduleId) {
let resolvePath = path.resolve(moduleId);
// 判断文件是否有后缀,如果没有则加上后缀
if (!path.extname(moduleId)) {
// 获取对象所有的key,返回一个对象所有key的集合
let arr = Object.keys(Module._extensions);
for (let i = 0;
i < arr.length;
i++) {
// 没有后缀将组成完整的文件路径
let file = resolvePath + arr[i];
try {
// 判断文件是否存在并且能够被访问
fs.accessSync(file);
return file;
} catch (error) {
console.log(error);
}
}
} else {
return resolvePath;
}
};
复制代码
第四步: 在拿到文件的绝对路径之后呢,我们将检查模块缓存中是否已经加载过这个模块,因此我们在类上定义了一个模块缓存对象:
// 模块缓存对象,是以模块的绝对路径作为key来进行缓存
Module._cacheModule = {};
复制代码
node模块导入时如何解析要导入的文件
如果缓存中没有要加载的模块对象,则构建一个模块,并读取模块的内容,在这里要注意的是:
- 不同文件模块加载和读取方式是不一样的
如上图所示,对于json类型的文件,我们需要将文件的内容读取出来并解析成JSON对象并挂载在module.exports对象身上即可第五步:
而对于js文件类型来说,有着独特的解析方式,所以我们根据文件的后缀来使用不同的加载方式:
// 根据不同文件类型加载模块
Module.prototype.load = function (filePath) {
// 获取文件后缀
let ext = path.extname(filePath);
// 根据文件后缀调用不同的文件模块加载策略,读取文件的内容
let content = Module._extensions[ext](this);
return content;
}
复制代码
而 Module._extensions 这个对象中则存放着真正解析文件的具体方法:
// js模块包裹数组
Module._wrapper = ["(function(exports,require,module){", "})"];
// 文件模块加载策略对象,包括js文件和json文件
Module._extensions = {
".js": function (module) {
// 读取js文件中的js
let scripts = fs.readFileSync(module.id, "utf8");
let fn = Module._wrapper[0] + scripts + Module._wrapper[1];
// 在沙箱中运行js代码,将不受上下文环境的影响
vm.runInThisContext(fn).call(module.exports,module.exports,req,module);
return module.exports;
},
".json": function (module) {
// json文件为读取内容后进行解析
return JSON.parse(fs.readFileSync(module.id, "utf8"));
}
}
复制代码
最后,我们通过一张gif图来浏览下所有代码,并跑下最终的结果:
好了,关于node 模块导入就先提到这里,欢迎大家多多提问题,谢谢!
推荐阅读
- 考研英语阅读终极解决方案——阅读理解如何巧拿高分
- 如何寻找情感问答App的分析切入点
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus使用queryWrapper如何实现复杂查询
- python学习之|python学习之 实现QQ自动发送消息
- 孩子不是实现父母欲望的工具——林哈夫
- opencv|opencv C++模板匹配的简单实现
- Node.js中readline模块实现终端输入
- 如何在Mac中的文件选择框中打开系统隐藏文件夹