来自coderwhy的node课程笔记和《深入浅出nodejs》
2.模块化
2.1.模块化开发
-
事实上模块化开发最终的目的是将程序划分成一个个小的结构;
-
这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;
-
这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
-
也可以通过某种方式,导入另外结构中的变量、函数、对象等;
-
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
2.2.没有模块化带来的问题
早期没有模块化带来了很多的问题:比如命名冲突的问题
当然,我们有办法可以解决上面的问题:立即函数调用表达式(IIFE)
但是,我们其实带来了新的问题:
-
第一,我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
-
第二,代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
-
第三,在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;
所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。
我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码; 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性; JavaScript社区为了解决上面的问题,涌现出一系列好用的规范,接下来我们就学习具有代表性的一些规范。
2.3.CommonJS和node
Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
在Node中每一个js文件都是一个单独的模块;
这个模块中包括CommonJS规范的核心变量:exports、module.exports、require
我们可以使用这些变量来方便的进行模块化开发
模块化的导入和导出:
exports和module.exports可以负责对模块中的内容进行导出;
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
-
Node中的每个js文件都是一个单独的模块
在bar.js中定义的变量 , 在main.js中是拿不到的
//bar.js , 所有的变量存在bar.js中的,别的模块是拿不到的 const name = 'xxx'; const age = 18; let message = 'hello world'; function say (name) { console.log(name); } //main.js console.log(name);//报错,因为这个模块里面没有name这个变量
-
使用exports 和 require导出导入
//bar.js const name = 'xxx'; const age = 18; let message = 'hello world'; function say (name) { console.log(name); } exports.name = name; exports.age = age; exports.message = message; exports.say = say; //main.js // 类似于 bar=exports(exports就是bar.js中exports对象) const bar = require('./bar.js'); console.log(bar);//{ name: 'xxx', age: 18, message: 'hello world', say: [Function: say] } console.log(bar.name);
解析:
首先我们上面说到exports是一个特殊的全局对象,他们每个模块都有的,初始的情况下是一个空对象
当我们在bar.js中使用
exports.name = name; exports.age = age; exports.message = message; exports.say = say
的时候,我们就给这个对象添加了属性,此时的exports(注意:exports是个对象,所以exports本身是个引用,指向的是一个地址)
//exports对象 { name: 'xxx', age: 18, message: 'hello world', say: [Function: say] //注意,函数是object类型,say应该是一个引用(类似指针) , 指向的是一个内存空间,而内存空间里放的是 say对象 }
画图表示
所以此时的bar.js中的
exports
地址和main.js中bar
指向的同一块内存地址,所以能使用bar来访问导出的对象为了进一步验证他们是同一个对象,我们可以做一些操作
比如:在bar.js中延迟1s修改name , 然后再从main.js中延迟2s读出来,看看name是否改变
另外再从main.js中修改age , 再从bar.js中读出来
//bar.js const name = 'xxx'; const age = 18; let message = 'hello world'; function say (name) { console.log(name); } setTimeout(() => { // 一定要记住,不能使用name = 'zdd',因为导出的name是exports对象里面的 , 而且setTimeout是异步的,会先执行下面的东西,然后再来执行这个定时器里的,所以如果使用name='zdd'是没用的,除非name是对象.可以使用name.xxx = xxx(同样涉及到堆与栈的问题) exports.name = 'zdd'; }, 1000); exports.name = name; exports.age = age; exports.message = message; exports.say = say; //--------------------------------------------------------------------------- //main.js // 类似于 bar=exports(exports就是bar.js中exports对象) const bar = require('./bar'); console.log('没有修改前', bar.name); setTimeout(() => { console.log("2s后", bar.name); }, 2000); //输出的结果 //没有修改前 xxx //2s后 zdd
至于另一种验证就不做了,但是同样是成立的
-
module.exports和exports
//bar.js let name = 'xxx'; const age = 18; let message = 'hello world'; function say (name) { console.log(name); } exports.name = name; exports.age = age; exports.message = message; exports.say = say; console.log(module);
当执行文件的时候,查看打印出来的module
可以看到这个module中也有exports(初始的情况下是空的)。所以导出的对象应该是
module.exports
上面的那个图是没有
module.exports
的情况,但是如果有module.exports了,情况就不一样了!现在我们知道了,导出的一个模块中导出的东西是module.exports , 像上面分析的是没有出现module.exports,这样会默认
module.exports = exports
,然后再将module.exports对象导出但是如果们是直接使用如下
module.exports ={ name:'zdd', hobbies:['singing' , 'dancing'] }
那么是不是下面的语句就没用了
exports.name = name; exports.age = age; exports.message = message; exports.say = say;
exports对象是否就没用了呢?答案是肯定的,一定没用了!!!
验证一下:
let name = 'xxx'; const age = 18; let message = 'hello world'; function say (name) { console.log(name); } exports.name = name; exports.age = age; exports.message = message; exports.say = say; module.exports = { name: 'zdd', hobbies: ['singing', 'dancing'] } console.log(module);
打印出来的module对象
为了更清楚,我们看一下mian.js中使用require('./bar.js')拿到的共享对象
const bar = require('./bar'); console.log(bar);
此时的内存图应该修改为
2.3.2.require
(以下内容都来自《深入浅出nodejs》)
node在引入模块的时候,需要3个步骤
-
路径分析
-
文件定位
-
编译执行
在node中,模块分为2种
-
核心模块
核心模块部分在node源代码的编译过程中,编译进了二进制执行文件中。在node进程启动时,部分核心模块就被直接加入到了内存中,所以在这部分核心模块的引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以他的加载速度是最快的。
-
用户编写的模块,又称为文件模块
文件模块是在运行时动态加载的,需要完整的路径分析,文件定位,编译执行过程,速度比核心模块
优先从缓存加载
与前端浏览器会缓存静态脚本文件提高性能一样,node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,但是node缓存的是编译和执行后的对象。
不论核心模块或者是文件模块,require()方法对相同的二次加载都采用缓存优先的方式,这是第一优先级
。不同之处在于,核心模块的缓存检查先于文件模块的缓存检查
路径分析和文件定位
require()方法接受标识符作为参数,然后根据标识符进行模块查找
-
核心模块,如htp , fs , path等
-
.或..开始的相对路径文件模块
-
以/开始的绝对路径文件模块
-
非路径形式的文件模块
-
核心模块:核心模块的优先级仅次于缓存加载。如果想要加载一个与核心模块标识符相同的自定义模块,是不会成功的,必须选择一个不同的标识符,或者是换用路径方式。
-
路径形式的文件模块:以. 、..或\开始的标识符,被当做文件模块来处理。在分析路径的时候,
require()会将路径转化为真实路径,并以真实路径为索引,将编译执行后的结果存放到缓存
,试二次加载更快。因为指定了明确的位置,所以加载的速度慢于核心模块 -
自定义模块:一种特殊的文件模块,可能是一个自定义的文件或者是包,这种最费时。会以类似原型链的方式,先找当前目录下的node_module , 然后父级的 , 如果没找到再找父级的。。。直到找到为止,否则报错
文件定位
从缓存加载的优化策略使得二次引入的时候不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。
但是在定位的过程中,文件扩展名的分析,目录和包仍需要值得注意。
-
文件扩展名的分析:
require()在分析标识符的过程中,会出现不包含扩展名的情况。commonjs模块也允许不包含扩展名。这种情况下,node会按.js ,
.json , .node的次序进行补足扩展名,依次尝试。
在尝试的过程中,需要调用fs模块同步阻塞式的进行判断文件是否存在。因为node是单线程的,所以会引起性能问题。诀窍是如果
是.node , .json文件的时候,添加后缀名
。另外,同步配合缓存,可以大幅度缓解node在单线程中阻塞式的调用缺陷
-
目录分析和包
-
如果有后缀名,按照后缀名查找文件
-
没有后缀名
-
直接查找文件
-
查找.js文件
-
查找.json文件
-
查找.node文件
-
-
没有找到对应的文件,但找到了一个目录,此时会将其当成一个包来处理
-
去找package.json文件,通过JOSN.parse()解析出描述对象,从mian属性中取出文件进行定位,如果没有扩展名,进入扩展名分析步骤
-
如果没有package.json或者是main属性指定的文件名错误,再依次查找index.js , index.json ,index.node
-
-
模块编译
再node中,每个文件模块都是一个对象,定义如下
function Module (id, parent) {
this.id = id;
this.exports = {};//加载模块时导出的对象
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;//用来判断时候已经加载过的
this.children = [];
}
编译和执行是引入文件模块的最后阶段。定位到具体的文件之后,node会新建一个模块对象,然后根据路径进行载入和编译。
每个成功编译的模块都会将其文件路径作为索引缓存在Module._cache对象上,提高二次引入的性能