理解 CommonJS
CommonJS 是 一种模块规范,是一种用于 JavaScript 模块化的标准Node.js 实现了这种规范,在 CommonJS 规范中,每个文件都被视为一个独立的模块,模块内部的变量和函数默认是私有的,如果需要在其他模块中使用,需要通过 module.exports
导出,然后通过 require
来引入其他模块。它允许 JavaScript 代码实现模块化,让开发者将代码拆分为可重用的模块。
创建一个 CommonJS 模块
要创建一个 CommonJS 模块,只需在文件中定义您的函数、对象或变量,并使用 module.exports
对象将它们导出。
// greet.js
function greet(name) {
return `你好,${name}!`;
}
module.exports = greet;
导入一个 CommonJS 模块
要在另一个文件中使用上面的模块,您需要使用 require()
进行导入。以下是一个示例:
// app.js
const greet = require('./greet');
console.log(greet('小明')); // 输出:你好,小明!
CommonJS 的优势
模块化: 通过将代码分为更小、可管理的模块,实现更好的组织。
可重用性: 模块可以在应用程序的不同部分之间轻松重用。
依赖管理: 模块之间的明确依赖关系使得代码更易于理解和维护。
封装性: 每个模块都有自己的作用域,可以防止全局作用域的污染。
提几个问题
一个模块中 require exports 是哪里来的?
require 方法到底干了什么?
exports和module.exports 有什么区别?
require 只能加载 js文件么?
...
手写require
// app.js
const greet = require('./greet');
console.log(greet('小明')); // 输出:你好,小明!
分析
从上面的代码const greet = require('./greet'); 可以看出 require 是一个函数 并且有返回值,返回值就是 module.exports 导出的值
所以require 的样子大概可能是这样的
function require(...,module){
...
return module.export
}
而我们的模块 定义的时候代码
// greet.js
function greet(name) {
return `你好,${name}!`;
}
module.exports = greet;
文件中的 module 是哪里来的,为什么可以在js 文件中可以直接使用,在前端 可以直接使用的变为分为两种
-
全局变为
-
自己定义的局部变为(包括函数的形参)
所以 module 可能是全局变量也可能是 函数的形参,通过看node源码 的loader https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js
可以看到node对CommonJS的实现
伪代码如下
function Module(id = '',parent) {
this.id = id; // 是我们require的路径
this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
this.exports = {}; // 导出
this.filename = null; // 模块对应的文件名
this.loaded = false; // loaded用来标识当前模块是否已经加载
}
// require 真正的实现
Module.prototype.require = function (id) {
return Module._load(id);
}
Module._cache = Object.create(null);
Module._load = function (request) {
const filename = Module._resolveFilename(request); // 返回
// 先检查缓存,如果缓存存在且已经加载,直接返回缓存
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 如果缓存不存在,我们就加载这个模块
const module = new Module(filename);
// load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整
Module._cache[filename] = module;
module.load(filename);
return module.exports;
}
Module.prototype.load = function (filename) {
// 获取文件后缀名
const extname = path.extname(filename);
// 调用后缀名对应的处理函数来处理 对应下面的实现 (Module._extensions['.js'] = function (){} )
Module._extensions[extname](this, filename);
// 标记为已加载
this.loaded = true;
}
Module._extensions['.js'] = function (module, filename) {
//读取文件内容
const content = fs.readFileSync(filename, 'utf8');
// 具体处理js 文件
module._compile(content, filename);
}
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
Module.wrap = function (script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.prototype._compile = function (content, filename) {
// 将从fs读取出来的 js模块代码 包装在一个函数里面 函数有 function (exports, require, module, __filename, __dirname) 5个参数
const wrapper = Module.wrap(content); // 获取包装后函数体
// 下文有介绍
// `vm.runInThisContext()`方法接受两个参数:要编译和执行的代码字符串`wrapper`,以及一个可选的选项对象`{ ... }`。
// 执行完后返回一个 编译后 的函数体 将代码字符串变为 函数 类似于 eval()
const compiledWrapper = vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
});
const dirname = path.dirname(filename);
// 相当于compiledWrapper(this.exports, this.require, this,filename, dirname)
// 也就是 包装后函数体function (exports, require, module, __filename, __dirname) 的五个参数
// exports可以直接用module.exports,即this.exports
// this.require 也就是 Module类原型上的 require方法 Module.prototype.require
// this 也就是module对象
// __filename 是 文件的绝对路径 也就是传进来的filename 形参
// __dirname是 path.dirname(filename); 的返回值
// 通过 call 修改了函数的this 指向 指向module.exports,即this.exports 也就是在node中 写代码this 指向 module.exports
compiledWrapper.call(this.exports, this.exports, this.require, this,
filename, dirname);
}
// eg demo.js demo 文件所在位置/Users/test/Desktop
console.log('this:',this,'__filename:',__filename, '__dirname:',__dirname,'test');
// 上面代码的输出为
// this: {} __filename: /Users/test/Desktop/demo.js __dirname: /Users/test/Desktop
-
const compiledWrapper = vm.runInThisContext(wrapper, { ... });
这行代码定义了一个常量
compiledWrapper
,它将保存编译后的代码的结果。vm.runInThisContext()
方法接受两个参数:要编译和执行的代码字符串wrapper
,以及一个可选的选项对象{ ... }
。 -
wrapper
wrapper
是一个JavaScript代码字符串,它包含要执行的代码。这个字符串可以是任何有效的JavaScript代码,包括函数定义、变量声明等。 -
{ filename, lineOffset: 0, displayErrors: true }
这是
vm.runInThisContext()
方法的第二个参数,一个选项对象。它包含了一些可选的配置项:filename
:指定代码的文件名,用于错误堆栈跟踪和调试目的。lineOffset
:指定代码的行偏移量,用于错误堆栈跟踪和调试目的。displayErrors
:一个布尔值,指定是否在控制台上显示错误信息。
通过调用vm.runInThisContext()
方法,传入要执行的代码字符串和选项对象,代码将被编译并在当前上下文中执行。编译后的结果将被赋值给compiledWrapper
常量,可以在后续的代码中使用。如果在执行过程中发生错误,并且displayErrors
选项设置为true
,则错误信息将被显示在控制台上。
上述代码要注意我们注入进去的几个参数和通过call
传进去的this
:
- this:
compiledWrapper
是通过call
调用的,第一个参数就是里面的this
,这里我们传入的是this.exports
,也就是module.exports
,也就是说我们js
文件里面this
是对module.exports
的一个引用。
- exports:
compiledWrapper
正式接收的第一个参数是exports
,我们传的也是this.exports
,所以js
文件里面的exports
也是对module.exports
的一个引用。
- require: 这个方法我们传的是
this.require
,其实就是MyModule.prototype.require
,也就是MyModule._load
。
- module: 我们传入的是
this
,也就是当前模块的实例。
- __filename:文件所在的绝对路径。
- __dirname: 文件所在文件夹的绝对路径。
当执行 let testJson = require(test.json) 这段代码时 最后会走到下面的伪代码里面 通过fs模块读取json文件内容 然后JSONParse 返回。
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSONParse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
当执行 let test = require(test.node) 这段代码时 最后会走到下面的伪代码里面 (.node
文件是C++编译后的二进制文件,纯前端一般很少接触这个类型)
// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};
总结
//test.js
module.exports = "hello world";
// app.js
let hello = require(test.js)
console.log(hello) //hello world
上面这段代码 最终会被包裹为然后执行
(function (exports, require, module, __filename, __dirname) {
module.exports = "hello world";
})()
// 最终 return module.exports
解决了上面提问的 1 2 4
那么问题3 exports和module.exports 有什么区别?
在 没有给 exports重新赋值的情况下 exports 和 module.exports 一样
如果这样使用
exports = { num:10 }
他们就不再相等了 而require 始终返回的 是 module.exports
和下面这段代码表达了相同的意思
<script>
let a1 = {num:1};
a1.num = a1.num+1;
let c1 = a1
a1 = {num:3};
console.log(a1,c1); //a1为 {num:3} c1为 {num:2} 这里面 a1相当于 module.exports c1 相当于 exports
</script>
参考
https://segmentfault.com/a/1190000023828613
https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js