前言

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在 Node 环境中,一个 .js 文件就称之为一个模块(module)。


一、使用模块的好处

使用模块最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括 Node 内置的模块和来自第三方的模块。​

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。


二、Node的模块实现

在 Node 中引入模块,需要经历如下三个步骤:

  • 路径分析
  • 文件定位
  • 编译执行


在 Node 中,模块分为两类:

第一类是 Node 自身提供的模块,称为核心模块:fs、http等,就像 java 中的jdk 提供的核心类一样。

第二类是用户编写的模块,称为文件模块

核心模块部分在 Node 源代码的编译过程中,编译进了二进制执行文件。在 Node 进程启动时,核心模块就被直接加载进内存,所以这部分的模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。

文件模块在运行时动态加载,需要完整的路径分析,文件定位,编译执行过程,加载速度比核心模块慢。

以上介绍了模块加载过程和模块的类别划分,下面我们来看看 Node 加载模块的具体过程,如下图:

「Node学习笔记」Node.js的模块实现及编译_模块

Node 为了优化加载模块的速度,也像浏览器一样引入了缓存,对加载过的模块会保存到缓存内,下次再次加载时就会命中缓存,节省了对相同模块的多次重复加载。模块加载前会将需要加载的模块名转为完整路径名,查找到模块后将完整路径名保存到缓存,下次再次加载该路径模块时就可以直接从缓存中取得。

在 require 加载模块时,require 参数的标识符可以以文件类型结尾 require(“./test.js”),也可以省略文件类型 require(“./test”)。

对于省略类型的第二种写法,Node 首先会认为它是一个 .js 文件,如果没有查找到该 js 文件,然后会去查找 .json 文件,如果还没有查找到该 json 文件,最后会去查找 .node 文件,如果连 .node 文件都没有查找到,就该抛异常了。

Node 在执行 require 加载模块时是线程阻塞的,大家都知道 Node 是单线程执行的,如果长期阻塞的话系统其它任务就得不到执行了,所以为了加快 require 模块的加载,如果不是 .js 文件的话,在 require 的时候就把文件类型加上,这样 Node 就不会再去一一尝试了。


三、模块编译

在 Node 中,每个文件模块都是一个对象,具体定义如下:

「Node学习笔记」Node.js的模块实现及编译_前端_02


编译和执行是引入文件模块的最后一个阶段。

定位到具体的文件后,Node 会新建一个模块对象,然后根据路径载入并编译。

对于不同的文件扩展名,其载入方法也有所不同,具体如下:

  • .js文件:通过 fs 模块同步读取文件后编译执行。
  • .json文件:通过 fs 模块同步读取文件后,用 JSON.parse() 解析返回结果。
  • .node文件:这是用 C/C++ 编写的扩展文件,通过 dlopen() 方法加载最后编译生成的文件。
  • 其余扩展名的文件:它们都被当作 .js 文件载入。

每一个编译成功的模块都会将其文件路径作为索引缓存在 Module._cache 对象上,以提高二次引入的性能。

我们都知道,在浏览器中编写的 js 文件,如果变量定义不是在函数或对象内,就会存在污染全局变量的情况,例如下面这种方式定义的变量:

<script>

a = 'test';

</script>

等同于 window.a = ‘test’;


但我们在 Node 中的每个 .js 模块内并没有做任何其它处理,定义的变量怎么就不会污染全局环境了呢?还有在 Node 的模块内,怎么就可以直接使用module、require、exports、filename、 dirname等对象呢?

事实上,在编译的过程中,Node 对获取的 JavaScript 文件内容进行了头尾包装。

例如我们一开始写的 DateUtil.js,如下:

「Node学习笔记」Node.js的模块实现及编译_模块_03「Node学习笔记」Node.js的模块实现及编译_Node_04


编译包装后的文件是下面这样的:

「Node学习笔记」Node.js的模块实现及编译_模块_05

这样每个模块之间就进行了作用域隔离。


以上就是本期关于 Node.js 模块实现的分享,希望能给大家带来帮助。

下期给大家分享更多实战中的点滴,如果大家对此感兴趣,欢迎各位关注、留言,大家的支持就是我的动力!