在CommonJs规范提出之前,Javascript是没有模块系统的,这意味着我们很难开发大型的应用,因为代码的组织会比较困难。
首先CommonJS不是Node独有的东西,CommonJs是一种模块规范,定义了如何引用和导出模块,Nodejs只是实现了这个规范,CommonJS模块规范主要分为模块引用、模块定义和模块标识三个部分。
模块引用
模块引用就是我们可以通过require
引入其它的模块。
const { add } = require('./add'); const result = add(1 ,2);
模块定义
一个文件就是一个模块,模块里会提供两个变量,分别为module和exports。module为当前模块本身,exports为要导出的内容,同时exports为module的一个属性,即exports为module.exports。其他模块通过require导入的内容即为module.exports的内容。
// add.js exports.add = (a, b) => { return a + b; }
模块标识
模块标识即为require里面的内容,比如require('./add')
,则模块标识为./add
。
通过CommonJS构建的这套模块导入导出机制使得用户完全无需考虑变量污染,可以方便的构建大型应用。
Node的模块实现
Node实现了CommonJs规范,并且增加了一些自己需要的特性。Node为了实现CommonJs规范主要做了以下三件事情:
路径分析
文件定位
编译执行
路径分析
当执行require()的时候,require接收的参数即为模块标识符,node通过模块标识符来进行路径分析。路径分析的目的就是为了通过模块标识符找到这个模块所在的路径。首先,node的模块分为两类,分别是核心模块和文件模块。核心模块是node自带的模块,文件模块是用户编写的模块。同时文件模块又分为相对路径形式的文件模块、绝对路径形式的文件模块和非路径形式的文件模块(比如express)。
当node找到一个文件模块之后,会将这个模块编译执行并且缓存起来,大致原理是将这个模块的完整路径作为key,编译后的内容作为值,后续再第二次引入这个模块的时候就不需要再进行路径分析文件定位编译执行这几个步骤了,可以直接从缓存中读取编译好的内容。
// 缓存的模块示意: const cachedModule = { '/Usr/file/src/add.js': 'add.js编译后的内容', 'http': 'Node自带的http模块编译后的内容', 'express': '非路径形式自定义文件模块express编译后的内容' // ... }
当要查找require导入的模块时,查找模块的顺序是先查看缓存里是否已经有该模块,如果缓存里面没有再查看核心模块,然后再查找文件模块。其中路径形式的文件模块比较好查找,根据相对或绝对路径就可以得到完整的文件路径。非路径形式的自定义文件模块查找起来会相对麻烦一些,Node会从node_modules这个文件夹里去查找是否有这个文件。
node_modules这个目录在哪里呢,比如说我们当前执行的文件为/Usr/file/index.js;
/** * /Usr/file/index.js; */ const { add } = require('add'); const result = add(1, 2);
这个模块里我们有引入了一个add模块,这个add不是一个核心模块也不是一个路径形式的文件模块,那么这时候如何找到这个add模块呢。
module有一个paths的属性,查找add模块的路径在paths这个属性里,我们可以把这个属性打出来看一下:
/** * /Usr/file/index.js; */ console.log(module.paths);
我们在file目录下执行node index.js可以打印出paths的值。paths里的值是一个数组,如下:
[ '/Usr/file/node_modules', '/Usr/node_modules', '/node_modules', ]
即Node会依次从上面的目录里寻在是否包含add这个模块,原理和原型链类似。先从当前执行的文件的同级目录的node_modules文件夹里开始找,如果没找到或者没有node_modules这个目录,则继续往上级查找。
文件定位
路径分析和文件定位是搭配一起使用的,文件标识符可以是不带后缀的,也可能通过路径分析找到的是一个目录或者一个包,这个时候要定位到具体的文件需要一些额外的处理。
文件扩展名分析
const { add } = require('./add');
比如上面这段代码,文件标识符是不带扩展名的,这个时候node会依次查找是否存在.js、.json、.node文件。
目录和包分析
同样是上面这段代码,通过./add
查找到的可能不是一个文件,可能是一个目录或者包(通过判断add文件夹下是否有package.json文件来判断是目录还是包)。这个时候文件定位的步骤是这样的:
如果package.json里没有main字段,那么也会将index作为文件,然后进行扩展名分析找到对应后缀的文件。
模块编译
我们开发中主要遇到的模块为json模块和js模块。
json模块编译
当我们require一个json模块的时候,实际上Node会帮我们使用fs.readFilcSync去读取对应的json文件,得到json字符串,然后调用JSON.parse解析得到json对象,再赋值给module.exports,然后给到require。
js模块编译
当我们require一个js模块的时候,比如
// index.js const { add } = require('./add');
// add.js exports.add = (a, b) => { return a + b; }
这个时候发生了什么呢,为什么我们可以直接在模块里使用module、exports、require这些变量。这是因为Node在编译js模块的时候对模块的内容进行了首尾的包装。
比如add.js这个模块,实际编译的时候是会被包装成类似这样的结构:
(function(require, exports, module) { exports.add = (a, b) => { return a + b; } return module.exports; })(require, module.exports, module)
即我们编写的js文件是会被包装成一个函数,我们编写的只是这个函数里的内容,Node后续的包装的过程对我们隐藏了。这个函数支持传入一些参数,其中就包括require、exports和module。
当编译完js文件后,就会执行这个文件,node会将对应的参数传给这个函数然后执行,并且返回module.exports值给到require函数。
以上就是Node实现CommonJs规范的基本流程。