浅析Node的模块化
概述
Node应用由模块组成,采用了CommonJS模块规范。因此根据这个规范,文件皆模块,每个文件都有自己的作用于,模块的导出通过module.exports
和exports
方式导出,导出后模块的导入通过require
实现。
模块分类
Node的模块可分为三类
- 核心模块(内置模块):如fs,http,path等,无需安装,直接引入即可
- 第三方模块:一些别人写的第三方模块,需要通过node包管理工具安装
- 自定义模块:自己项目中实现,通过绝对路径或者相对路径引入。
用法示例
1.通过module.exports
可以导出一个整体对象
// a.js
module.exports = {
a:1
};
// b.js 引入a.js
let a = require('./a')
console.log(a); // {a:1}
2.通过exports
导出
// a.js
exports.a = 1;
// b.js 引入a.js
let a = require('./a')
console.log(a); // {a:1}
由于上述两种导出,结果一致。 接下来对exports
及module.exports
进行一些小测试
1.module.exports和exports默认是一个空对象
console.log(module.exports); // {}
console.log(exports); // {}
2.从下面可以看出,exports和module.exports实际上是指向的同一处引用地址.
console.log(exports === module.exports); // true
3.平时写法都是module.exports = {};而exports都是对某个属性进行赋值;尝试对exports进行重新赋值, 结果打印始终未空对象
// a.js
exports = 1;
// b.js
let a = require('./a')
console.log(a); // {}
// 上面的导出并没有作用到最终的导出对象,可大致理解成,node的模块化规范,默认导出的是module.exports,而exports只是引用了module.exports,所以修改了exports的取值并不会影响到module.exports,所以也不会影响到最终的效果
4.每个文件默认导出一个对象,多处引用,其他地方对内容进行改动会改动整体的取值。下面的例子中,b.js导入了a.js,并将a属性改成了99999
, 在c.js中引入c后对a模块进行查看,发现a的值发生了改变。
// a.js
exports = 1;
// b.js
let a = require('./a');
a.a = 99999;
// c.js
let a = require('./a')
console.log(a); // {a:1}
let c = require('./c.js');
console.log(a); // {a: 99999}
进一步探索
经过上面的例子后,对过程处理一头雾水,并且为啥node环境下支持使用__dirname
和__filename
呢?决定了解下里面的原理。
对require函数进行断点调试,然后进入查看, 发现内部的源码是一个helpers.js
的文件,调用了里面的makeRequireFunction
产生的require
函数,该函数接收一个path
路径参数,返回mod.require
函数调用后的返回值。
// Invoke with makeRequireFunction(module) where |module| is the Module object
// to use as the context for the require() function.
function makeRequireFunction(mod) {
const Module = mod.constructor;
function require(path) {
try {
exports.requireDepth += 1;
return mod.require(path);
} finally {
exports.requireDepth -= 1;
}
}
function resolve(request, options) {
if (typeof request !== 'string') {
throw new ERR_INVALID_ARG_TYPE('request', 'string', request);
}
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
function paths(request) {
if (typeof request !== 'string') {
throw new ERR_INVALID_ARG_TYPE('request', 'string', request);
}
return Module._resolveLookupPaths(request, mod, true);
}
resolve.paths = paths;
require.main = process.mainModule;
// Enable support to add extra extension types.
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
继续往下走,mod.require
实际上是mod
是由Module
构造出来的实例,Module
原型上绑了require
方法。函数内调用了Module
的静态方法_load
。
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
if (typeof id !== 'string') {
throw new ERR_INVALID_ARG_TYPE('id', 'string', id);
}
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
return Module._load(id, this, /* isMain */ false);
};
- Module._load加载模块。
- Module._resolveFilename将路径转换成绝对路径
- let module = new Module,创建一个模块,初始化id和exports对象等信息。
- tryModuleLoad,尝试加载模块
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
// filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
//
var filename = Module._resolveFilename(request, parent, isMain);
// 查看是否有缓存,有缓存直接走缓存处理,不往后执行
var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
// 创建模块
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 将当前模块存储起来
Module._cache[filename] = module;
// 尝试加载模块
tryModuleLoad(module, filename);
return module.exports;
};
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
- 通过不同的后缀对文件进行处理
// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);
assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 通过不同的后缀进行加载 .json .js .node .mjs
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
if (experimentalModules) { // 是否是实验性模块
// ...
}
};
- 不同文件后缀处理逻辑不同
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
// content是自己编写的文件内容,通过读取后返回的字符串
content = stripShebang(content);
// create wrapper function
// 结合Module.wrap函数,将内容进行包裹处理.
var wrapper = Module.wrap(content);
// 执行包裹后的函数
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var inspectorWrapper = null;
if (process._breakFirstLine && process._eval == null) {
// ...省略
}
var dirname = path.dirname(filename);
var require = makeRequireFunction(this);
var depth = requireDepth;
if (depth === 0) stat.cache = new Map();
var result;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
require, this, filename, dirname);
} else {
// 执行函数
result = compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
}
if (depth === 0) stat.cache = null;
// 返回结果
return result;
};
_compile函数的过程是先读取用户自己写的文件内容,然后将内容包裹成一个新的函数,并执行返回结果。
例如:
// 我编写的内容 my.js
console.log('hello world');
- 经过_compile处理第一步,读取到的content为’console.log(“hello world”)’;
- 经过Module.wrap的包裹, 得到一个字符串函数’(function (exports, require, module, __filename, __dirname) {console.log(‘hello world’);\n}’
- 该函数有5个参数, exports就是module.exports, require是导入模块函数, module即当前模块,__filename当前文件名,__dirname当前文件夹名
- 经过node内置模块vm的runInThisContext执行该代码,返回一个隔离作用域的函数compiledWrapper
- 执行compiledWrapper函数,并将this指向修改为this.exports, 实际上就是module.exports.
通过上面这些代码,就能明白用node运行时, exports和module.exports的关系,以及__filename和__dirname是如何产生的.