node.js模块简明详解

什么是模块?

我们先简单描述下模块的特点,模块是一个独立的完成某些功能的单位,它应该具有抽象性、封装性(接口),例如u盘就是一个模块,为什么这么所呢,
1.独立性:我们将文件存入u盘,它是可以独立完成文件的存储功能。
2.抽象性:u盘就是将便携式文件存储的这项常用、公用的功能提取出来的产物。
3.封装性:u盘里面有哪些零件,往往从它外面是看不见的,这里这么说会有些歧义,严谨点说就是u盘里面的那些我们不需要关心的零件,u盘都包装了起来,对外只会留给我们一个使用的usb接口(接口在封装性中非常重要)

js模块

js的模块化的发展是一个演进的过程,目的是为了解决命名冲突、文件依赖等问题,一些原始的写法、模块化的工具sea.js、requireJs、再到webpack等工具这些模块化的知识大家可以参考其他资料,这里笔者不赘述,只说关键的、跟要说的node.js模块相关的,先来看代码

    var foo=(function(){
        var bar=123;
        var add=function(v1,v2){
           return v1+v2;
        }
        return {
           add:add
        }
   )()

上面的代码就是js的一个模块,独立完成某些功能和抽象,这里不用说了,就是一个简单add方法,我们重点说下封装,这里使用return的方式对外开放接口, 而在函数里面的其他代码例如bar,就属于被函数封装了。那么为什么要说这一段js代码,其实不管AMD、CMD、commonJs规范或者es6的模块,它们的实现的核心就是函数自调用的这种封装。

node.js模块

首先说node.js模块是采用CommonJS模块规范,那么和上面js模块有什么样的关系,先不用着急,我们先看下下面这个图:


F8CAB508-5665-468D-AB99-C38B9ECE3CBB.png

这是在Chrome控制台上定义一个foo,window对象就会多一个属性foo,我们在页面上加载的js代码都有这样的特点,因为window是全局对象。
对比一下node.js的代码,node.js也有一个全局对象global,会不会和window一样呢,我们看下下面的代码:

foo = 123;
var bar = 456;
console.log('foo--->',global.foo); 
console.log('bar--->',global.bar);

输出结果:


D4DDCDCA-8919-446F-B437-BAC7B1B565C2.png

在一个js里面去定义全局变量foo,foo会变成global的属性,而bar没有,bar变成这个js私有的了,就如同下面这张图:


47BD6253-3382-4A05-97EC-D395B0B2C227.png

在node.js里面,我们把一个js就看成是一个模块,它具有封装性,在这个模块里面的代码(全局变量除外)都是私有的,如果想要被外部调用,那就需要exports与module.exports曝露出去,并且用require去接收。

module.exports与exports

一个node.js模块中也就是一个js中(下面我们都用一个模块代替一个js),module.exports用法如下

//用法1
//直接赋值,可以赋数字、字符串、数组、对象、函数
//注意:一个模块中module.exports只能赋值一次
module.exports=123;
//用法2
//属性赋值,可以多次赋值,赋数字、字符串、数组等都可以
module.exports.foo=123;
module.exports.bar=456;

exports的用法如下

//用法
//exports只能通过属性的方式曝露,曝露数字、字符串、数组等都可以
exports.foo=123;

require

有了代码的曝露,那么也就有代码的引入,require就是用来加载模块的,这里我们先不谈模块化系统,只说加载一个简单模块,module.exports与require如下

//1.js
module.exports=‘囧’
//2.js
var foo=require(‘./1’);
console.log(foo); //输出囧

exports与require如下

//3.js
exports.bar=‘囧’
//4.js
var foo=require(‘./1’);
console.log(foo.bar); //输出囧

通过require得到的就是module.exports,永远是module.exports,那exports呢?module.exports与exports的区别是什么呢?,exports是module.exports的别名,可以理解为 var exports=module.exports,所以exports只能用于属性的赋值,应用场景也是不同的,如果我只想从一个模块中曝露出一个字符串或者一个数字,那要用module.exports,如果用exports则会向上面的4.js那样需要通过属性的形式取,显的很繁琐,如果是用来曝露多个属性,虽然module.exports可以用属性曝露,最好还是用exports,因为写起来简单...对就是这个原因----简单,如下

//5.js
module.exports.foo=123;
module.exports.bar=123;
//6.js
exports.foo=123;
exports.bar=123;

两种写法都可以,所以一次性曝露用module.exports,如果是曝露多个属性、方法、字符串等用exports比较简单。

模拟require

上面我们还遗留了两个问题,1. var bar = 456;这个bar是怎么在一个模块中变成私有的?2.为什么exports是module.exports的别名?
那么下面我们简单模拟下require方法,如下

var fs = require('fs');
//模拟require方法
var myRequire = function (path) {
    var Module = function () {
        this.exports = {}
    }
    var code = fs.readFileSync(path);
    //包装代码
    var packageCode = `(function(exports ,module) {${code}  return module.exports})`;
    console.log(packageCode);
    //获取要执行的方法
    var result = eval(packageCode);
    console.log(result);
    var module=new Module();
    //将module.exports当实参传给exports
    return result(module.exports,module);
}
//调用
var foo = myRequire('./foo.js');
console.log(foo);

首先我们用fs模块将要引入的foo.js的代码读出来,然后包装成下面的代码

(function(exports ,module) {
//foo.js里面的代码
 return module.exports})

现在我们就清楚了为什么一个js里面的代码是具有封装性的了,用的是js模块那提到过的函数函数自调用的方式封装的。

 return result(module.exports,module);

注意我们去调用result这个方法,result指向的就是(function(exports ,module) {//foo.js里面的代码 return module.exports})这个方法,那么这个方法的形参就是exports、module,实参是module.exports、module,也就是说我们在代码里面使用的exports,其实是指向的module.exports。

var Module = function () {
        this.exports = {}
    }
  var module=new Module();

这里我们看到一个module对象,这个对象是用来存储每一个模块的信息,我们可以在模块的代码里面打印下module看一下

console.log(module);
340235D0-BB0D-44F9-833E-019FF690EC82.png

如上图,module里面的exports默认是一个空对象,和我们模拟的写法一致,这也就是我们可以直接是引用module.exports.属性曝露接口的原因,parent、children是用来存被引用和引用的模块的,paths这是npm查找模块的路径,每一个被node加载的模块都有唯一的一个module存储着这个模块的信息,也说明module是动态产生的。笔者在这里只谈基础的模块,至于模块化系统,请参考其他文章。

推荐阅读更多精彩内容