今天,我们来聊聊 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导入的核心代码,那么这里都做了些什么事情呢,我们一一来分析:

  • 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 模块导入就先提到这里,欢迎大家多多提问题,谢谢!