早期的JavaScript因为缺少模块系统。要编写JS脚本,必须依赖HTML对其进行管理,严重制约了JavaScript的发展。而CommonJS规范的提出,赋予了JavaScript开发大型应用程序的基础能力。其中NodeJS借鉴CommonJS的Modules规范实现了一套简单易用的模块系统,为JavaScript在服务端的开发开辟了道路。html
CommonJS的模块规范定义三个了部分:node
模块引用:模块所在的上下文提供require方法,可以接受模块标识为参数引入一个模块的API到当前模块的上下文中。json
模块定义:在模块中,存在一个module对象以表明模块自己,同时存在exports做用模块属性的引用。api
模块标识:模块标识即为require方法的参数,它要求必须为小驼峰命名的字符串,相对路径或绝对路径。数组
模块引用:在Node模块的上下文中,存在require方法,可以对模块进行引入,如const fs = require("fs");
。浏览器
模块定义:在Node中,以单个文件做为模块的基础单位,即一个文件为一个模块,全部挂载到exports对象上的方法属性即为导出。缓存
// person.js
exports.name = "vincent";
exports.say = function() {
console.log("hello world");
};
// driver.js
const person = require("person");
exports.say = function() {
person.say();
console.log(`I am ${person.name}`);
};
复制代码
模块标识:Node将为模块分为两类,一类是由Node提供的内建模块,也称为核心模块;另外一类是用户编写的模块,称为用户或第三方模块。而Node中的模块标识符主要分为如下几类。bash
以上即是Node对CommonJS模块规范的实现概览。但实际上Node对模块规范进行了必定的取舍,在require
和exports
module
过程当中加入了自身的特点,下面让咱们来深刻了解一下:app
require
实现的核心逻辑(省略了部分代码)Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
// 存在父级模块时,拼接路径做为临时缓存索引(查询真实路径)
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
// 在缓存中查询模块
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
// 将模块push到父级模块的children数组中
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 删除临时索引
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// 查询模块真实路径,策略同步骤二
const filename = Module._resolveFilename(request, parent, isMain);
const cachedModule = Module._cache[filename];
// 缓存存在时,返回module.exports
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 缓存不存在时,优先查找核心模块
const mod = loadNativeModule(filename, request, experimentalModules);
// 若是能够被开发者直接reuqire, 那么直接返回module.exports
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// 生成模块实例并缓存
const module = new Module(filename, parent);
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
// 是否加载成功,默认失败
let threw = true;
try {
// 加载模块,根据文件后缀名使用对应方法
// .js -> fs.readFileSync -> compile (下一个章节会说明)
// .json -> fs.readFileSync -> JSON.parse
// .node -> fs.readFileSync -> dlopen (C/C++模块)
// 其余类型省略
module.load(filename);
threw = false;
} finally {
// 加载失败,删除缓存及其索引
if (threw) {
delete Module._cache[filename];
if (parent !== undefined) {
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
}
return module.exports;
};
复制代码
Module._findPath = function(request, paths, isMain) {
// 绝对路径
const absoluteRequest = path.isAbsolute(request);
if (absoluteRequest) {
paths = [''];
} else if (!paths || paths.length === 0) {
return false;
}
// 尝试经过路径缓存索引获取
const cacheKey = request + '\x00' +
(paths.length === 1 ? paths[0] : paths.join('\x00'));
const entry = Module._pathCache[cacheKey];
if (entry) return entry;
// 判断路径是否以":/"或/结尾
var exts;
var trailingSlash = request.length > 0 &&
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
if (!trailingSlash) {
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
}
// For each path
for (var i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist
const curPath = paths[i];
if (curPath && stat(curPath) < 1) continue;
var basePath = resolveExports(curPath, request, absoluteRequest);
var filename;
// 查询文件类型
var rc = stat(basePath);
if (!trailingSlash) {
// 文件是否存在
if (rc === 0) { // File.
// 尝试根据模块类型获取真实路径,代码省略
filename = findPath();
}
// 尝试给文件添加后缀名
if (!filename) {
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryExtensions(basePath, exts, isMain);
}
}
// 当前文件路径为文件目录且后缀名不存在,尝试获取filename/index[.extension]
if (!filename && rc === 1) { // Directory.
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryPackage(basePath, exts, isMain, request);
}
// 缓存路径并返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 没有找到文件,返回false
return false;
};
复制代码
// (function(exports, require, module, __filename, __dirname) {\n
JS文件代码...
// \n})
复制代码
这样每一个模块文件直接都进行了做用域隔离。包装事后的代码经过vm原生模块的
runInThisContext()
返回一个具体的function对象。最后将当前模块对象的module
自身引用,require
方法,exports
属性及一些等全局属性做为参数传入function
中执行。这就是这些变量没有定义却在每一个模块文件中存在的缘由。框架
刚开始接触Node时,对于module.exports
和exports
的关系会存在一些疑惑。它们均可以挂载属性方法,做为当前模块的导出。但它们分别表示什么?又有什么区别呢?观察下面的代码:
// a.js
exports.name = "vincent";
module.exports.name = "ziwen.fu";
exports.age = 24;
// b.js
const a = require("a");
console.log(a); // { "name": "ziwen.fu", age: 24 };
复制代码
从前面的模块源码解析中,咱们能够得知Node模块最终导出的上module.exports
的值,而从上面的代码咱们能够肯定,module.exports
的初始值为{}
,而exports
是做为module.exports
的引用,挂载到exports
上的属性方法,最终会由module.exports
导出。继续观察下面的代码:
// a.js
exports = "from exports";
module.exports = "from module.exports";
// b.js
const a = require("a");
console.log(a); // from module.exports
复制代码
从代码运行结果能够看出,直接对module.exports
和exports
进行赋值,最终模块导出的是module.exports
的值。从上一小结可知,exports
在当前模块上下文中是做为形参传入,直接改变形参的引用,并不能改变做用域外的值。测试代码以下:
const myModule = function(myExports) {
myExports = "24";
console.log(myExports);
};
const myExports = "8";
myModule(myExports); // 24
console.log(myExports); // 8
复制代码
下面这段来自Node官网的示例:
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
// main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
// console.log
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
复制代码
官方的解释是,当main.js加载a.js时,a.js尝试加b.js。此时b.js又尝试去加载a.js。为了不死循环,a.js导出了一个未完成的副本,使b.js完成加载,随后b.js导出给a.js完成整个过程。目前ES6 Module 已经提供了解决方案,Node的 ES6模块也已经进入测验阶段,这里就不作过多介绍了。
上面介绍了Node模块的基础机制,大多数状况下咱们可能都会使用依赖前置的方式去require
模块,即提早引入当前模块所需的模块,并它置于代码顶部。但有时候,存在部分模块,程序不须要当即使用它们,这时候动态引入是一个更好的选择。下面是Node Web服务框架egg.js中加载器(Loader)实现的相关代码,它提供了一个不同的思路:
// egg-core/lib/loader/utils
loadFile(filepath) {
// filepath来自于require.resolve(path)的定位
try {
// 非JavaScript模块,同步读取文件
// Module._extension为Node模块支持后缀名数组
const extname = path.extname(filepath);
if (extname && !Module._extensions[extname]) {
return fs.readFileSync(filepath);
}
// JavaScript模块直接require
const obj = require(filepath);
if (!obj) return obj;
// ES6模块返回处理
if (obj.__esModule) return 'default' in obj ? obj.default : obj;
return obj;
} catch (err) {
// ...
}
}
// egg-core/lib/loader/context_loader
// 代理上下文中对象app.context
// property对应项目文件名
Object.defineProperty(app.context, property, {
get() {
// 查询缓存
if (!this[CLASSLOADER]) {
this[CLASSLOADER] = new Map();
}
const classLoader = this[CLASSLOADER];
// 获取模块对象实例并缓存
let instance = classLoader.get(property);
if (!instance) {
instance = getInstance(target, this);
classLoader.set(property, instance);
}
return instance;
},
});
复制代码
egg-loader的思路是经过代理app.context上的property,动态的去require
模块并加以包装以后挂载到上下文对象上。其中property是来源于require.resolve
定位的模块文件名,借助缓存机制,使得程序在运行过程当中可以按需引入模块,同时也减小了开发者引入模块以及维护模块名及路径的成本。
目前Node的模块机制中,require
模块是基于readFileSync
实现的同步API,对于大文件的引入存在诸多不便。而正在试验过程当中的ES Module
支持异步动态引入,同时也解决了循环依赖的问题,将来可能将普遍应用到Node模块机制中。