Node-2-模块系统(node学习笔记,模块系统是后续所有内容的基础)

502 阅读10分钟

什么是模块?关键词(数据私有,暴露接口)

公共的js文件,即将一个复杂的程序依据一定的规则封装成几个块文件,并组合在一起,模块的内部数据、实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信。

原始模块化写法:

在没有CommonJSES6之前的时候,想要达到模块化的效果有这么3种。

一个函数就是一个模块

function m1{
    //...
}

function m2{
    //...
}

缺点:污染了全局变量,无法保证不会与其他模块发生冲突,而且模块成员之间看不出直接关系。

一个对象就是一个模块

为了解决函数模块的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

var module1 = new Object({
    _sum:0,
    foo1:function(){},
    foo2:function(){},
})

缺点:会暴露所有模块成员,内部的状态可能被改写。例如我们如果只想暴露两个方法foo而不暴露出_sum就做不到,而此时可以通过 module1. _sum=2,来改写该内部属性。

立即执行函数为一个模块

var module = (function(){
    var _sum = 0
    var foo1 = function(){}
    var foo2 = function(){}
    return {
        foo1:foo1,
        foo2:foo2
    }
})()

利用立即执行函数内的作用域以及闭包来实现模块化功能,导出我们想要的成员,此时外部就不能访问到 _sum属性了。

Node中模块的导入与导出

node采用的commonjs的模块系统, 导入模块用的是require这个全局函数,导出模块用的是module.exports的方式, require方法导入本地的某个文件组件的话, 一定要加上盘符前缀(即便是在同一目录下)。

以对象形式导出如下: 新exports对象形式导出.png

以对象形式导入如下: 新require对象形式导入.png

直接导出如下: 直接导出.png

直接导入如下: 直接导入.png

module对象

module是什么1.png Node内部提供一个Module构造函数。所有模块都是Module的实例,如上图所示。

module是什么2.png 每个模块内部,都有一个module对象,代表当前模块。如上图所示,它有以下属性:

module.id 模块的识别符,通常是带有绝对路径的模块文件名。

module.filename 模块的文件名,带有绝对路径。

module.loaded 返回一个布尔值,表示模块是否已经完成加载。

module.parent 返回一个对象,表示调用该模块的模块。

module.children 返回一个数组,表示该模块要用到的其他模块。

module.exports 表示模块对外输出的值。

module.parent里面的Module.paths是查找导入模块路径顺序的优先级,如果在这一级中没有找到,就依次往上级查找,直到找到或者找不到为止。

exports变量

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同(只是等同于,并不是真的有这行代码)在每个模块头部,有一行这样的命令:let exports = module.exports;

这样造成的结果是,在对外输出模块接口时,可以向exports对象添加方法,如下:

// 代码示例
// 注意是在exports对象上添加了一个方法
exports.area = function(r){
	return Math.PI * r * r;
}

// 注意是在exports对象上添加了一个方法
exports.circumference = function(r){
    return 2 * Math.PI * r;
}

注意,不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系,以下是错误示范

// 要注意这是错误的写法!因为这里直接将exports指向了一个新的值,切断了exports与原来module.exports的联系
exports = function(x){
	console.log(x);
}

require指令

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports所导出的对象。如果没有发现指定模块,则会得到一个空对象。

如下图所示: require是什么.png

require命令用于加载文件,后缀名默认为.js

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。 require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件;如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件index.node文件

例如下图: require导入路径步骤.png

在上图中:

第1步,我们先在service.js文件中使用require方法,找到当前目录下的tool文件夹下的math文件夹

第2步,自动查看math文件夹下的package.json文件

第3步,通过package.json文件中的main字段,找到指定的入口文件,即当前目录下的index.js文件

第4步,所以在第1步中let add = require('./tool/math'),就相当于是let add = require('./tool/math/index.js'),即service.js文件require导入并赋值给变量add保存的对象,是从index.js文件中所exports导出的对象{add:[Function:add]}

第5步,所以在终端中node执行service.js文件,打印输出的就是对象{add:[Function:add]}

根据参数的不同格式,require命令去不同路径寻找模块文件。

(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js

(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js

(3)如果参数字符串不以“./”“/”开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)

(4)如果参数字符串不以“./”“/”开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径

(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js.json.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析

(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法

模块的补充信息

模块的缓存

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

require导入对象会缓存.png

上面代码中,连续两次使用require命令,加载同一个模块。第一次加载的时候之后,为输出的对象添加了一个a属性,值为1。但是第二次加载的时候,这个a属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存

其原因就是因为在第一次require导入模块时,会得到一个对象,后续再require导入同一个模块时,会检测是否已经引用过了,如果已经存在了一个之前导入过的对象,后续的引用就直接拿到该对象进行赋值,而对象的赋值,是引用类型的赋值,所以指向的都是同一个对象。

如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数

删除模块缓存

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写:

删除模块缓存.png

(注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。)

循环加载(引用)问题

如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本,如下图:

循环引用步骤.png

完整解析如下:

service.js内代码如下:

console.log("service.js",require("./tool/a")) 
// 第一步执行上面这行代码,但是需要require获取到a.js文件的数据,所以先跳转到a.js文件中去执行

// 从第七步跳转回来,带着刚刚从a.js文件中获取的exports.x的值为"a2",继续上面这行代码在控制台输出打印输出:service.js {x:"a2"}

console.log("service.js",require("./tool/b"))
//第八步执行上面这行代码,打印输出:service.js {x:"b2"}

a.js内代码如下:

exports.x = "a1" // 第二步,跳转到a.js文件的第一行代码,此时exports.x的值为"a1" 

console.log("a.js",require("./b.js")) 
// 第三步,紧接着执行上面这行代码,但是这行代码中需要require获取到b.js的数据,所以又需要先跳转到b.js文件

// 从第六步跳转回来,带着刚刚从b.js文件中获取的exports.x的值为"b2",此时上面这行代码在控制台输出打印:a.js {x:"b2"}

exports.x = "a2" // 第七步,继续执行这行代码,将exports.x的值赋值为"a2",此时a.js文件中所有代码都执行完了,service.js文件中第一步代码所需要获取值也都能全部获取到了,此时再跳转回service.js文件执行剩下的代码

b.js代码如下:

exports.x = "b1" // 第四步,跳转到b.js文件的第一行代码,此时exports.x的值为"b1"

console.log("b.js",require("./a.js"))
// 第五步,紧接着执行上面这行代码,但是这行代码中需要require获取到a.js的数据,所以又得跳回a.js文件获取其中exports.x的值,但是a.js文件中执行的代码依旧停留在第三步,我们要获取a.js文件中exports.x的值也只能往上面代码第二步找,第二步中exports.x的值为"a1" 。所以这个时候b.js文件的这行代码只能拿到a.js文件中exports.x的值为"a1",所以在控制台上最先console打印输出:b.js {x:"a1"}

exports.x = "b2" // 第六步,执行这行代码,将exports.x的值赋值为"b2",此时b.js文件中所有代码都执行完了,a.js文件中第三步代码所需要获取值也都能全部获取到了,此时再跳转回a.js文件执行剩下的代码

模块的运行机制

模块的加载机制:

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(记住仅仅只是普通的值, 要是引用类型的值的话,还是受影响的)。普通数据类型的模块加载就是值类型,直接复制这个值,发生变化不会影响原来的值,而引用数据类型的值的模块加载就是引用该值的引用地址,发生变化后会影响原来的值。请看下图中的例子:

引用类型和普通类型的导入导出.png

require的内部处理流程:

require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load。如下所示:

image.png

上面的第4步,采用module.compile()执行指定模块的脚本,逻辑如下:

image.png

上面的第1步和第2步,require函数及其辅助方法主要如下:

1、require(): 加载外部模块

2、require.resolve():将模块名解析到一个绝对路径

3、require.main:指向主模块

4、require.cache:指向所有缓存的模块

5、require.extensions:根据文件的后缀名,调用不同的执行函数