~ Node.js 模块成员的引入与导出
引入模块
在 JS 中,加载模块使用 script 标签即可;在 Node.js 中,如何在一个模块中,加载另一个模块呢?
// 使用 require 方法进行引入,文件的扩展名( 通常为.js .json .node )可省略
require('./math'); // require 方法,通过路径分析进行模块的路径查找,从而找到这个'包'所在位置
# 缓存加载
与前端浏览器会缓存静态脚本文件以提高性能一样,Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而 Node 缓存的是编译和执行之后的对象。
不论是核心模块还是文件模块,require() 方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。
# 标识符分析
require() 方法接受一个标识符作为参数。Node 实现中,正是基于这样一个标识符进行模块查找的。
模块标识符在Node中主要分为以下几类:
- 核心模块,如http、fs、path等;
- 以.或..开始的相对路径文件模块;
- 以/开始的绝对路径文件模块;
- 非路径形式的文件模块,如自定义的connect模块
根据参数的不同格式,
require命令去不同路径寻找模块文件
- 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,
require('/home/marco/foo.js')将加载/home/marco/foo.js- 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,
require('./circle')将加载当前脚本同一目录的circle.js- 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)
// 如果是当前路径下的文件模块,一定要以./开头,否则nodejs会试图去加载核心模块,或node_modules内的模块
# 文件扩展名分析
require() 在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS 模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node 会先查找是否存在没有后缀的该文件,如果没有,再按 .js、.json、.node(较少,一般是通过C++编写的模块,二进制文件)的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用 fs 模块同步阻塞式地判断文件是否存在。因为 Node 是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是 .node 和 .json 文件,在传递给 require() 的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解 Node 单线程中阻塞式调用的缺陷。
# 目录分析和包
在分析标识符的过程中,require() 通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时 Node 会将目录当做一个包来处理。
在这个过程中,Node 对 CommonJS 包规范进行了一定程度的支持。首先,Node 在当前目录下查找package.json( CommonJS 包规范定义的包描述文件 ),通过 JSON.parse() 解析出包描述对象,从中取出 main 属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。
而如果 main 属性指定的文件名错误,或者压根没有 package.json 文件,Node 会将 index 当做默认文件名,然后依次查找 index.js、index.json、index.node。如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。
导出模块
如何在一个模块中访问另外一个模块中定义的变量呢?
[1] 全局对象
最容易想到的是:把一个模块中定义的变量复制到 global 中,然后,另一个模块访问全局环境
不过,这种方法虽然简单,但由于会污染全局环境,不推荐使用
// a.js var a = 100; global.a = a; // b.js require('./a'); console.log(global.a); // 100
[2] module 对象
推荐使用的是:Node.js 提供的模块对象 module,该对象保存了当前模块相关的一些信息:
module.id 模块的识别符,通常是带有绝对路径的模块文件名 module.filename 模块的文件名,带有绝对路径 module.loaded 返回一个布尔值,表示模块是否已经完成加载 module.parent 返回一个对象,表示调用该模块的模块 module.children 返回一个数组,表示该模块要用到的其他模块 module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容
module.exports表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量即,使用 require() 方法导入模块时,导入的结果,永远以 module.exports 指向的对象为准
// test.js let a = 100; function add(b,c){ return b + c; } module.exports = { a,add }// app.js var test = require('./test.js'); console.log(test.a); // 100 console.log(test.add(1,2)); // 3
「
exports」为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports。造成的结果是,在对外输出模块接口时,可以向 exports 对象添加方法;
但是,不能直接将 exports 变量指向一个值,因为这样等于切断了
exports与module.exports的联系。console.log(module.exports === exports); // true
编译模块
编译和执行是模块实现的最后一个阶段,定位到具体的文件后,Node.js 会新建一个模块对象,然后根据路径载入并编译。不同的文件扩展名,其载入方法也有所不同:
- js 文件 — 通过 fs 模块同步读取文件后编译执行;
- node 文件 — 这是用 C/C++ 编写的扩展文件,通过 dlopen() 方法加载最后编译生成的文件;
- json 文件 — 通过 fs 模块同步读取文件后,用 JSON.parse() 解析返回结果;
- 其余扩展名文件 — 它们都被当做 .js 文件载入
每一个编译成功的模块都会将其文件路径作为索引缓存在 Module._cache 对象上,以提高二次引入的性能 ...