简单记录以下Node.js的模版机制。
Node.js使用的是CommonJS的模版规范,CommonJS和JavaScript的关系如图:
原生js被限制在浏览器的沙盒中,很多功能都缺失,比如最重要的IO功能,这导致了js虽然在前端中无敌,却迟迟不能进入后端开发。
CommonJS是由一系列的库组成的,是对js缺失功能的补充。
CommonJS模块规范
模块导入
CommonJS使用require()方法来实现对模块引用,该方法接受一个模块标识字符串,引入这个模块到程序上下文中,例如:
const fs = require('math');
一般使用const来接收一个模块,以防模块受到修改。
模块标识
模块标识是一个字符串,其实就是模块的路径。
模版标识是省去后缀名的,也就是不需要.js后缀名。
模版标识中的路径可以是相对路径,也可以是绝对路径,这一般是自定义的模版。
- 对于标准库就需要前缀,直接用它的名字就行,比如:
require('fs')
- 相对路径,以
.
、..
、/
等形式找到需要调用的模版相对于当前文件的路径,比如:require('../math/add');
- 绝对路径,从操作系统的根目录出发,找到需要调用的模版位置,比如:
require('D:/example/lib/math/add');
模块导出
exports对象是导出模块的唯一出口,它被定义在任何CommonJS的文件中。
在CommonJS中一个文件就是一个模块。
-
可以分开导出,例如:
exports.add = (a,b) => { return a+b; } exports.sub = (a,b) => { return a-b; }
-
也可以先申明,然后最后再一起导出,例如:
function add(a,b){ return a+b; } function sub(a,b){ return a-b; } module.exports = { add: add, sub: sub }
从这个例子我们还可以得出一个结论,其实exports只是module的一个属性,module指的是这个文件。
另外在模版内部和模版外部可以用不一样的函数名,例如:
module.exports = { sub: add, add: sub }
当然,千万不要写这样作用和名字不符的API,不然调用你模版的人会打死你。
模版底层架构
require底层的实现非常复杂,这里简单阐述一下。
我们把node提供的模版称为核心模块,用户编写的模块称为文件模块。
模块的加载主要由以下三步组成:
-
路径分析
解析模块路径,转换成真实路径,放入缓存中,第二次调用可以加速。
-
文件定位
因为模版定义的时候没有指定后缀名,所以确定后缀名。
node会按后缀名为.js、.node、.json的顺序查找路径和名字匹配的文件。 -
编译执行
-
对于js文件,仅仅在外面包装一层函数,以防止全局环境污染。
-
对于node文件,因为本就是C/C++模版编译生成的,所以不需要编译。经过libuv兼容层,还能让node文件跨平台。
-
对于json文件,采用JSON.parse()解析成js对象。
-
模版在第一次运行时三步都要执行,之后借助node的缓存机制,可以直接运行。并且缓存区的调用优先级高于未加载过的模版。
核心模块加载只有路径分析这一步要做,因为大多数库都是底层C/C++封装的,是经过编译的二进制文件,放在内存中。
而且核心模块的优先级高于文件模块,所以你命名一个和标准库一样的模版是不会调用成功的。