node.js模块化


作者:Seiya

时间:2019年05月28日


前言

Node.js 模块系统中,每个文件都被视为一个独立的模块。

Node.js 模块机制采用了 Commonjs 规范,弥补了当前 JavaScript 开发大型没有标准的缺陷,类似于 Java 中的类文件,Python 中的 import 机制,NodeJs 中可以通过 module.exportsrequire 来导出和引入一个模块。

在模块加载机制中,NodeJs 采用了延迟加载的策略,只有在用到的情况下,系统模块才会被加载,加载完成后会放到 binding_cache 中。



模块的分类


系统模块

C/C++模块,也叫built-in内建模块,一般用于native模块调用,在require出去

native模块,在开发中使用的Nodejs的http、buffer、fs等,底层也是调用的内建模块(C/C++)。


第三方模块

这里非Nodejs自带的模块称为第三方模块,其实还分为路径形式的文件模块(以.、..、/开头的)和自定义的模块(比如express、koa框架、moment.js等)



模块加载机制


在Nodejs中模块加载一般会经历3个步骤,路径分析文件定位编译执行


按照模块的分类,按照以下顺序进行优先加载:

  • 系统缓存:

    模块被执行之后会会进行缓存,首先是先进行缓存加载,判断缓存中是否有值。


  • 系统模块:

    也就是原生模块,这个优先级仅次于缓存加载,部分核心模块已经被编译成二进制,省略了路径分析、文件定位,直接加载到了内存中,系统模块定义在Node.js源码的lib目录下,可以去查看。


  • 文件模块:

    优先加载.、..、/开头的,如果文件没有加上扩展名,会依次按照.js、.json、.node进行扩展名补足尝试,那么在尝试的过程中也是以同步阻塞模式来判断文件是否存在,从性能优化的角度来看待,.json、.node最好还是加上文件的扩展名。


  • 目录做为模块:

    这种情况发生在文件模块加载过程中,也没有找到,但是发现是一个目录的情况,这个时候会将这个目录当作一个包来处理,Node这块采用了Commonjs规范,先会在项目根目录查找package.json文件,取出文件中定义的main属性("main": "lib/hello.js")描述的入口文件进行加载,也没加载到,则会抛出默认错误: Error: Cannot find module 'lib/hello.js'


  • node_modules目录加载:

    对于系统模块、路径文件模块都找不到,Node.js会从当前模块的父目录进行查找,直到系统的根目录


require模块加载时序图



模块循环引用


看以下例子:假设有a.js、b.js两个模块相互引用,会有什么问题?是否为陷入死循环?a模块中的undeclaredVariable变量在b.js中是否会被打印?

// a.js
console.log('a模块start');
exports.test = 1;
undeclaredVariable = 'a模块未声明变量'
const b = require('./b');
console.log('a模块加载完毕: b.test值:',b.test);

// b.js
console.log('b模块start');
exports.test = 2;
const a = require('./a');
console.log('undeclaredVariable: ', undeclaredVariable);
console.log('b模块加载完毕: a.test值:', a.test);

最终输出结果:

a模块start
b模块start
undeclaredVariable:  a模块未声明变量
b模块加载完毕: a.test值: 1
a模块加载完毕: b.test值: 2

答案:

启动a.js的时候,会加载b.js,那么在b.js中又加载了a.js,但是此时a.js模块还没有执行完,返回的是一个a.js模块的exports对象未完成的副本给到b.js模块。然后b.js完成加载之后将exports对象提供给了a.js模块

因为undeclaredVariable是一个未声明的变量,也就是一个挂在全局的变量,那么在其他地方当然是可以拿到的。



模块封装器


在执行模块代码之前,Node.js 会使用一个如下的函数封装器将其封装:

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

通过这样做,Node.js 实现了以下几点:

  • 它保持了顶层的变量(用 varconstlet 定义)作用在模块范围内,而不是全局对象。

  • 它有助于提供一些看似全局的但实际上是模块特定的变量,例如: - 实现者可以用于从模块中导出值的 moduleexports 对象。 - 包含模块绝对文件名和目录路径的快捷变量 __filename__dirname



exports与module.exports的区别


在一个node执行一个文件时,会给这个文件内生成一个 exports 和 module 对象,而 module 又有一个 exports 属性。他们之间的关系如下图,都指向一块{}内存区域。如下所示:

exports = modules.exports = {};

简而言之,区分他们之间的区别就是 exports 只是 module.exports 的引用,辅助后者添加内容用的。用白话讲就是,exports只辅助 module.exports 操作内存中的数据,最后真正被 require 出去的内容还是 module.exports 的。


CommonJS定义的模块分为:

模块标识(module)、模块定义(exports) 、模块引用(require)

最后更新时间: 2019-7-7 21:55:38