如何实现|如何实现 node module 模块导入

今天,我们来聊聊 node 的模块,主要内容分别有:

1.什么是模块化?模块化都有哪些规范?
2.node 模块导入具体是如何实现的?
模块化历史 单例模式
  • 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 实现单例核心思想 用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
  • 单例模式抽象,分离创建对象的函数和判断对象是否已经创建
var getSingle = function (fn) { var result; return function () { return result || ( result = fn.apply(this, arguments) ); } }; 复制代码

形参 fn 是我们的构造函数,我们只要传入任何自己需要的构造函数,就能生成一个新的惰性单例。
闭包
  • 函数执行后返回一个引用空间这个空间被外部引用,此空间无法销毁。这就叫闭包 函数执行的时候也会产生一个闭包
AMD
  • 异步模块定义
  • 并非JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。
requireJS主要解决两个问题
  • 1、多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
  • 2、js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。
CMD
  • 通用模块定义
  • CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。
AMD和CMD区别:
  • 1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
  • 2、CMD推崇就近依赖,只有在用到某个模块的时候再去require。
Commonjs
  • 1.模块实现(一个 js 文件就是一个模块)为了实现模块化的功能每个文件外面都包含一个闭包。
  • 2.规定如何导出一个模块 module.exports。
  • 3.如果导入一个模块 require。
node 模块导入实现步骤
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"对象:

步骤小结: 通过上面的分析我们不难发现:
  • 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 模块导入就先提到这里,欢迎大家多多提问题,谢谢!

    推荐阅读