一、CommonJS的模块规范
(1)模块引用
在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中
(2)模块定义
// main.js
exports.add = () => {
console.log('my name is main');
}
// program.js
const main = require('./main');
main.add(); // my name is main
require() ==方法==用来引入模块
exports ==对象==用于导出当前模块的方法或者变量,并且它是唯一的导出出口
module 代表模块自身,而exports是module的属性
console.log(exports === module.exports); // true
在Node中,一个文件就是一个模块。
(3)模块标识
传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以 . 、.. 开头的相对路径,或者绝对路径。它可以没有文件名后缀。
模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中(详见JavaScript模块的编译),同时支持引入和导出功能以顺畅地连接上下游依赖。
二、模块实现
在Node种引入模块,需要经历3个步骤:路径分析、文件定位、编译执行。
模块两类:
核心模块:Node提供,加载最快
文件模块:用户编写
加载顺序:
(1)缓存
模块加载一次便会缓存,与浏览器不同的是,浏览器缓存的是文件,Node缓存的是编译和执行之后的对象。
(2)路径分析 ==(加载速度从快到慢)==
- 核心模块: 提前编译为二进制文件,所以加载速度最快,如http、fs、path等。 路径形式的文件模块:指明了真实路径,所以加载速度仅次于核心模块。
- 自定义模块:
模块路径生成规则:
-
当前文件目录下的node_modules目录
-
父级目录下的node_modules目录
-
父级的父级目录下的node_modules目录
-
沿路径向上逐级递归,直到根目录下的node_modules目录
-
具体递归路径可以通过module.paths看出
console.log(module.paths);
// 输出
[
'c:\\Users\\123\\Desktop\\node\\TDD\\test\\node_modules',
'c:\\Users\\123\\Desktop\\node\\TDD\\node_modules',
'c:\\Users\\123\\Desktop\\node\\node_modules',
'c:\\Users\\123\\Desktop\\node_modules',
'c:\\Users\\123\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
(3)文件定位
- 文件拓展名分析:
如果require()参数不包含拓展名,则按照.js、.json、.node的次序补足拓展名,依次尝试。使用fs模块同步阻塞式的判断文件是否存在。
- 目录分析和包
如果没有找到对应文件却找到一个目录,则会查找该目录下的==package.json文件==,通过JSON.parse()解析出包描述对象,从中取出==main属性==指定的文件名进行定位。如果文件名缺少拓展名,则会进入拓展名分析步骤。
如果main属性指定文件名错误,或压根没有package.json文件,==Node会将index当作默认文件名==,然后依次查找index.js、index.json、index.node。 如果
如果目录分析的过程中没有定位成功任何文件,则进入下一个模块路径进行查找。如果模块路径数组都遍历完毕,依然没有查找到目标文件,则抛出查找失败的异常。
(4)模块编译
定位到具体文件后,Node会新建一个module对象,然后根据路径载入并编译。每一个编译成功的模块,都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
module对象
console.log(module);
// 输出
Module {
id: '.',
path: 'c:\\Users\\123\\Desktop\\node\\TDD\\test',
exports: {},
parent: null,
filename: 'c:\\Users\\123\\Desktop\\node\\TDD\\test\\program.js',
loaded: false,
children: [],
paths: [
'c:\\Users\\123\\Desktop\\node\\TDD\\test\\node_modules',
'c:\\Users\\123\\Desktop\\node\\TDD\\node_modules',
'c:\\Users\\123\\Desktop\\node\\node_modules',
'c:\\Users\\123\\Desktop\\node_modules',
'c:\\Users\\123\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
}
缓存对象
// program.js
const main = require('./main');
const Module = require("module");
console.log(Module._cache);
// 输出
// 路径为索引,对应的module对象为值
{
'c:\\Users\\123\\Desktop\\node\\TDD\\test\\program.js': Module {
id: '.',
path: 'c:\\Users\\123\\Desktop\\node\\TDD\\test',
exports: {},
parent: null,
filename: 'c:\\Users\\123\\Desktop\\node\\TDD\\test\\program.js',
loaded: false,
children: [ [Module] ],
paths: [
'c:\\Users\\123\\Desktop\\node\\TDD\\test\\node_modules',
'c:\\Users\\123\\Desktop\\node\\TDD\\node_modules',
'c:\\Users\\123\\Desktop\\node\\node_modules',
'c:\\Users\\123\\Desktop\\node_modules',
'c:\\Users\\123\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
},
'c:\\Users\\123\\Desktop\\node\\TDD\\test\\main.js': Module {
id: 'c:\\Users\\123\\Desktop\\node\\TDD\\test\\main.js',
path: 'c:\\Users\\123\\Desktop\\node\\TDD\\test',
exports: { add: [Function (anonymous)] },
parent: Module {
id: '.',
path: 'c:\\Users\\123\\Desktop\\node\\TDD\\test',
exports: {},
parent: null,
filename: 'c:\\Users\\123\\Desktop\\node\\TDD\\test\\program.js',
loaded: false,
children: [Array],
paths: [Array]
},
filename: 'c:\\Users\\123\\Desktop\\node\\TDD\\test\\main.js',
loaded: true,
children: [],
paths: [
'c:\\Users\\123\\Desktop\\node\\TDD\\test\\node_modules',
'c:\\Users\\123\\Desktop\\node\\TDD\\node_modules',
'c:\\Users\\123\\Desktop\\node\\node_modules',
'c:\\Users\\123\\Desktop\\node_modules',
'c:\\Users\\123\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
}
}
Node默认支持编译.js、.json、.node文件,如果想编译其他格式的文件,可以通过require.extensions['.xxx']或Module._extensions进行扩展。
编译txt文件
// abc.txt
我是一个txt文件
// main.js
const fs = require("fs");
const Module = require("module");
Module._extensions[".txt"] = (module, filename) => {
const content = fs.readFileSync(filename, "utf-8");
try {
module.exports = content;
} catch (err) {
err.message = filename + ":" + err.message;
throw err;
}
};
const a = require("./abc.txt");
console.log(a); // 我是一个txt文件
1. JavaScript模块的编译
我们知道每个模块文件中存在着==require、exports、module、__filename、__dirname==这5个变量,但是他们在模块中并没有定义,那么从何而来呢?
事实上,在编译过程中,Node会将模块文件的内容进行头尾包装,包装成如下格式:
包装前
const path = require("path");
const src = path.resolve(__dirname, "src");
console.log(src); // c:\Users\123\Desktop\node\TDD\test\src
exports.area = (r) => {
return Math.PI * Math.pow(r, 2);
};
包装后
(function (exports, require, module, __filename, __dirname) {
const path = require("path");
const src = path.resolve(__dirname, "src");
console.log(src); // c:\Users\123\Desktop\node\TDD\test\src
exports.area = (r) => {
return Math.PI * Math.pow(r, 2);
};
});
这样再通过外层将require、exports、module、__filename、__dirname这5个变量通过参数的形式传进了,就可以很优雅的使用了。并且这种方式还起到了作用域隔离的作用,使得每个模块都有独立的空间,互不干扰,在引用时也显得干净利落。
但是新的问题又产生了,被函数包裹的区域还是可以访问并修改父级乃至全局作用域的变量,如下面这段代码所示。
let num = 1;
function setNum() {
num = 99;
}
setNum();
console.log(num); // 99
可以看到,在setNum()方法里任然可以修改父级作用域中的num变量,还是没有起到完全隔离的效果,让我们来看看Node是怎么做的。
Node将function包装后的代码会通过字符串的形式传给vm原生模块的==runInThisContext()方法==执行,返回一个具体的function对象。最后将模块对象的require、exports、module、__filename、__dirname传这个function()执行,返回最后的exports对象。
runInThisContext的作用是建立一个沙箱环境,避免污染父级作用域。
// 使用runInThisContext
const vm = require("vm");
let num = 1;
vm.runInThisContext("function setNum() {num = 99;}");
setNum();
console.log(num); // 1
console.log(global.num); // 99
题外知识: 我们知道web中定义变量如果不写var、let、const关键字, 则会将该变量赋值给window属性。同理在Node中也存在全局变量global, 此时沙箱中的num被赋值给了gloabal对象。
此外,可能你会产生疑问,为什么存在exports的情况下,我们却通常使用module.exports导出模块呢?
因为exports是作为形参的方式传入的,在JavaScript规范中,不推荐我们直接修改形参。并且如果你一不小心给形参重新赋值的话,便只能改变形参的引用,不会改变作用域外的值,这种操作将不会导出任何东西。所以最好使用module.exports导出模块。
2. C/C++模块的编译
Node调用process.dlopen()方法进行加载和执行,加载过程中,Node将C/C++文件编译为.node模块,执行过程中exports与.node模块产生联系,返回给调用者。
C/C++模块的优势是执行效率更快,劣势是编写门槛较高。
3. JSON文件的编译
Node利用fs模块同步读取JSON文件内容后,调用JSON.parse()方法得到对象,最后通过exports导出。