모듈은 Node.js에서 매우 기본적이고 중요한 개념입니다. 모듈을 통해 다양한 네이티브 클래스 라이브러리가 제공되며, 타사 라이브러리도 모듈을 통해 관리되고 참조됩니다. 이 기사는 기본 모듈 원리부터 시작하여 결국 이 원리를 사용하여 간단한 모듈 로딩 메커니즘을 직접 구현합니다. 즉, require
을 직접 구현합니다.
Node는 JavaScript 및 commonjs 모듈을 사용하며 npm/yarn을 패키지 관리자로 사용합니다.
【동영상 튜토리얼 추천: node js tutorial】
기존 규칙, 원리를 설명하기 전에 간단한 예를 들고 이 예부터 시작하여 단계별로 원리를 심화시켜 보겠습니다. Node.js에서 특정 콘텐츠를 내보내려면 module.exports
를 사용해야 합니다. module.exports
를 사용하면 문자열과 문자열을 포함한 거의 모든 유형의 JS 개체를 내보낼 수 있습니다. 함수, 객체, 배열 등 먼저 가장 간단한 hello world
:module.exports
,使用module.exports
几乎可以导出任意类型的JS对象,包括字符串,函数,对象,数组等等。我们先来建一个a.js
导出一个最简单的hello world
:
// a.js module.exports = "hello world";
然后再来一个b.js
导出一个函数:
// b.js function add(a, b) { return a + b; } module.exports = add;
然后在index.js
里面使用他们,即require
他们,require
函数返回的结果就是对应文件module.exports
的值:
// index.js const a = require('./a.js'); const add = require('./b.js'); console.log(a); // "hello world" console.log(add(1, 2)); // b导出的是一个加法函数,可以直接使用,这行结果是3
当我们require
某个模块时,并不是只拿他的module.exports
,而是会从头开始运行这个文件,module.exports = XXX
其实也只是其中一行代码,我们后面会讲到,这行代码的效果其实就是修改模块里面的exports
属性。比如我们再来一个c.js
:
// c.js let c = 1; c = c + 1; module.exports = c; c = 6;
在c.js
里面我们导出了一个c
,这个c
经过了几步计算,当运行到module.exports = c;
这行时c
的值为2
,所以我们require
的c.js
的值就是2
,后面将c
的值改为了6
并不影响前面的这行代码:
const c = require('./c.js'); console.log(c); // c的值是2
前面c.js
的变量c
是一个基本数据类型,所以后面的c = 6;
不影响前面的module.exports
,那他如果是一个引用类型呢?我们直接来试试吧:
// d.js let d = { num: 1 }; d.num++; module.exports = d; d.num = 6;
然后在index.js
里面require
他:
const d = require('./d.js'); console.log(d); // { num: 6 }
我们发现在module.exports
后面给d.num
赋值仍然生效了,因为d
是一个对象,是一个引用类型,我们可以通过这个引用来修改他的值。其实对于引用类型来说,不仅仅在module.exports
后面可以修改他的值,在模块外面也可以修改,比如index.js
里面就可以直接改:
const d = require('./d.js'); d.num = 7; console.log(d); // { num: 7 }
require
和module.exports
不是黑魔法我们通过前面的例子可以看出来,require
和module.exports
干的事情并不复杂,我们先假设有一个全局对象{}
,初始情况下是空的,当你require
某个文件时,就将这个文件拿出来执行,如果这个文件里面存在module.exports
,当运行到这行代码时将module.exports
的值加入这个对象,键为对应的文件名,最终这个对象就长这样:
{ "a.js": "hello world", "b.js": function add(){}, "c.js": 2, "d.js": { num: 2 } }
当你再次require
某个文件时,如果这个对象里面有对应的值,就直接返回给你,如果没有就重复前面的步骤,执行目标文件,然后将它的module.exports
加入这个全局对象,并返回给调用者。这个全局对象其实就是我们经常听说的缓存。所以require
和module.exports
并没有什么黑魔法,就只是运行并获取目标文件的值,然后加入缓存,用的时候拿出来用就行。再看看这个对象,因为d.js
是一个引用类型,所以你在任何地方获取了这个引用都可以更改他的值,如果不希望自己模块的值被更改,需要自己写模块时进行处理,比如使用Object.freeze()
,Object.defineProperty()
之类的方法。
这一节的内容都是一些概念,比较枯燥,但是也是我们需要了解的。
Node.js的模块有好几种类型,前面我们使用的其实都是文件模块
function MyModule(id = '') { this.id = id; // 这个id其实就是我们require的路径 this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径 this.exports = {}; // 导出的东西放这里,初始化为空对象 this.filename = null; // 模块对应的文件名 this.loaded = false; // loaded用来标识当前模块是否已经加载 }
a.js
를 빌드한 다음 함수를 내보내는 b.js
를 만들어 보겠습니다. : 🎜MyModule.prototype.require = function (id) { return Module._load(id); }
index.js
, 즉 🎜그들에서 사용하세요. 🎜함수에 의해 반환된 결과는 해당 파일 module.exports
의 값입니다. 🎜MyModule._cache = Object.create(null); MyModule._load = function (request) { // request是我们传入的路劲参数 const filename = MyModule._resolveFilename(request); // 先检查缓存,如果缓存存在且已经加载,直接返回缓存 const cachedModule = MyModule._cache[filename]; if (cachedModule !== undefined) { return cachedModule.exports; } // 如果缓存不存在,我们就加载这个模块 // 加载前先new一个MyModule实例,然后调用实例方法load来加载 // 加载完成直接返回module.exports const module = new MyModule(filename); // load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整 MyModule._cache[filename] = module; module.load(filename); return module.exports; }
module.exports
만 가져오는 것이 아닙니다. 하지만 처음부터 실행합니다. module.exports = XXX
파일은 실제로는 한 줄의 코드일 뿐입니다. 나중에 설명하겠지만 이 코드 줄의 효과는 실제로 < 모듈의 code>export 속성입니다. 예를 들어 또 다른 c.js
를 살펴보겠습니다. 🎜MyModule._resolveFilename = function (request) { const filename = path.resolve(request); // 获取传入参数对应的绝对路径 const extname = path.extname(request); // 获取文件后缀名 // 如果没有文件后缀名,尝试添加.js和.json if (!extname) { const exts = Object.keys(MyModule._extensions); for (let i = 0; i < exts.length; i++) { const currentPath = `${filename}${exts[i]}`; // 如果拼接后的文件存在,返回拼接的路径 if (fs.existsSync(currentPath)) { return currentPath; } } } return filename; }
c.js
에서 우리는 c
를 내보냈는데, 이 c</ code> code>여러 단계의 계산 후 <code>module.exports = c;
행까지 실행하면 c
의 값은 2
입니다. 그래서 우리는 🎜 c.js
의 값은 2
입니다. c
의 값을 6
으로 변경해도 영향을 받지 않습니다. the Previous 이 코드 줄:🎜MyModule.prototype.load = function (filename) { // 获取文件后缀名 const extname = path.extname(filename); // 调用后缀名对应的处理函数来处理 MyModule._extensions[extname](this, filename); this.loaded = true; }
c.js
의 변수 c
는 기본 데이터 유형이므로 다음 c = 6;< /code>는 이전 <code>module.exports
에 영향을 미치지 않습니다. 참조 유형이면 어떻게 되나요? 직접 시도해 보겠습니다. 🎜MyModule._extensions['.js'] = function (module, filename) { const content = fs.readFileSync(filename, 'utf8'); module._compile(content, filename); }
index.js
에서 🎜He: 🎜module.exports = "hello world";
d.num이 <code>module.exports
다음에 제공되는 것을 발견했습니다. code>d
는 객체이자 참조 유형이고 이 참조를 통해 해당 값을 수정할 수 있으므로 할당은 여전히 유효합니다. 실제로 참조 유형의 경우 해당 값은 module.exports
뒤에서 수정될 수 있을 뿐만 아니라 모듈 외부에서도 수정될 수 있습니다. 예를 들어 index.js</code 내에서 직접 수정할 수 있습니다. > : 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false">function (module) { // 注入module变量,其实几个变量同理
module.exports = "hello world";
}</pre><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div><h2 id="item-2">🎜 및 <code>module.exports
는 흑마법이 아닙니다 🎜🎜이전 예인 🎜 및 module.exports</에서 볼 수 있습니다. code >우리가 하는 일은 복잡하지 않습니다. 먼저 전역 개체 <code>{}
가 있다고 가정해 보겠습니다. 처음에는 파일을 생성할 때 파일을 꺼내서 실행합니다. this 파일에 module.exports
가 있습니다. 이 코드 줄을 실행하면 module.exports
값이 이 개체에 추가됩니다. 마지막으로 개체는 다음과 같습니다. 🎜MyModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
module.exports
를 추가합니다. 이 전역 개체를 추가하고 호출자에게 반환합니다. 이 전역 개체는 실제로 우리가 자주 듣는 캐시입니다. 따라서 🎜과 module.exports
사이에는 마법이 없습니다. 단지 실행하여 대상 파일의 값을 얻은 다음 캐시에 추가하고 필요할 때 꺼내기만 하면 됩니다. 이 개체를 다시 살펴보세요. d.js
는 참조 유형이므로 이 참조를 얻을 때마다 해당 값을 변경할 수 있습니다. Object.freeze()
, Object.defineProperty()
와 같은 메서드를 사용하는 등 모듈을 직접 작성할 때 변경 사항을 처리해야 합니다. 🎜파일 모듈
입니다. , 주로 두 가지 유형이 있습니다: 🎜
- 내장 모듈:
fs
,http
등과 같이 Node.js에서 기본적으로 제공하는 함수입니다. 이 모듈은 Node.js가 로드될 때 로드됩니다. .js 프로세스가 시작됩니다.fs
,http
等等,这些模块在Node.js进程起来时就加载了。- 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
加载顺序是指当我们require(X)
时,应该按照什么顺序去哪里找X
,在官方文档上有详细伪代码,总结下来大概是这么个顺序:
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去
node_modules
下面找。- 还找不到就报错了。
前面提到找不到文件就找文件夹,但是不可能将整个文件夹都加载进来,加载文件夹的时候也是有一个加载顺序的:
- 先看看这个文件夹下面有没有
package.json
,如果有就找里面的main
字段,main
字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他package.json
里面的main
字段吧,比如jquery
的main
字段就是这样:"main": "dist/jquery.js"
。- 如果没有
package.json
或者package.json
里面没有main
就找index
文件。- 如果这两步都找不到就报错了。
require
主要支持三种文件类型:
- .js:
.js
文件是我们最常用的文件类型,加载的时候会先运行整个JS文件,然后将前面说的module.exports
作为require
的返回值。- .json:
.json
文件是一个普通的文本文件,直接用JSON.parse
将其转化为对象返回就行。- .node:
.node
文件是C++编译后的二进制文件,纯前端一般很少接触这个类型。
require
前面其实我们已经将原理讲的七七八八了,下面来到我们的重头戏,自己实现一个require
。实现require
其实就是实现整个Node.js的模块加载机制,我们再来理一下需要解决的问题:
- 通过传入的路径名找到对应的文件。
- 执行找到的文件,同时要注入
module
和require
这些方法和属性,以便模块文件使用。- 返回模块的
module.exports
本文的手写代码全部参照Node.js官方源码,函数名和变量名尽量保持一致,其实就是精简版的源码,大家可以对照着看,写到具体方法时我也会贴上对应的源码地址。总体的代码都在这个文件里面:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js
Node.js模块加载的功能全部在Module
类里面,整个代码使用面向对象的思想,如果你对JS的面向对象还不是很熟悉可以先看看这篇文章。Module
类的构造函数也不复杂,主要是一些值的初始化,为了跟官方Module
名字区分开,我们自己的类命名为MyModule
node_modules
아래의 모듈은 모두 파일 모듈입니다. 로딩 순서는 require(X)
를 의미합니다. html#modules_all_together" rel="nofollow noreferrer" target="_blank">자세한 의사 코드</a>를 검색하면 요약하자면 대략 다음과 같은 순서입니다. 🎜🎜🎜🎜내장 모듈은 다음과 같습니다. 먼저 로드되며, 같은 이름의 파일이 있어도 내장 모듈이 먼저 사용됩니다. </li>🎜내장 모듈이 아니므로 먼저 캐시로 이동하여 찾으세요. </li>🎜캐시가 없다면 해당 경로의 파일을 찾아보세요. </li>🎜해당 파일이 없으면 이 경로가 폴더로 로드됩니다. </li>🎜해당 파일과 폴더를 찾을 수 없다면 <code>node_modules
로 이동하여 찾아보세요. 🎜찾을 수 없어서 오류신고를 했는데요.
앞서 말씀드린 것처럼 파일을 찾을 수 없다면 그냥 폴더를 찾아보세요, 폴더 전체를 로드하는 것은 불가능합니다. 폴더를 로드할 때 로드 순서도 있습니다. 🎜🎜🎜🎜먼저 이 폴더 아래에 package.json
이 있는지 확인하세요. < 내부 코드>기본 필드를 찾아 기본
필드에 값이 있으면 해당 파일을 로드합니다. 따라서 일부 타사 라이브러리의 소스 코드를 볼 때 입구를 찾을 수 없는 경우 package.json
에서 main
필드를 살펴보세요. jquery
main
필드는 "main": "dist/jquery.js"
와 같습니다. 🎜package.json
이 없거나 package.json
에 main
이 없으면 index를 찾으세요.
파일. 🎜이 두 단계 중 어느 것도 찾을 수 없으면 오류가 보고됩니다.
require
는 주로 세 가지 파일 형식을 지원합니다. 🎜🎜 🎜🎜🎜.js🎜: .js
파일은 가장 일반적으로 사용되는 파일 형식입니다. 로드 시 전체 JS 파일이 먼저 실행된 다음 module.exports
가 실행됩니다. 위에서 언급한 code>가 require
의 반환 값으로 실행됩니다. 🎜🎜.json🎜: .json
파일은 일반 텍스트 파일입니다. JSON.parse
를 사용하여 객체로 변환하고 반환하면 됩니다. 🎜🎜.node🎜: .node
파일은 C++로 컴파일된 바이너리 파일입니다. 순수 프런트 엔드는 일반적으로 이 유형과 거의 접촉하지 않습니다.
require
사실 앞서 이미 원칙에 대해 자세히 설명했습니다. 자, 이제 우리의 하이라이트인 require
를 직접 구현해 보겠습니다. require
를 구현하는 것은 실제로 Node.js 전체의 모듈 로딩 메커니즘을 구현하는 것입니다. 해결해야 할 문제를 살펴보겠습니다. 🎜🎜🎜🎜 들어오는 경로 이름을 통해 해당 파일을 찾습니다. 🎜찾은 파일을 실행하는 동시에 모듈 파일을 사용할 수 있도록 module
및 require
메서드와 속성을 삽입합니다. 🎜모듈의 module.exports
로 돌아가기
이 글의 손으로 쓴 코드는 모두 공식 Node.js 소스 코드를 참조합니다. , 그리고 함수명과 변수명은 최대한 비슷하게 작성하여 일관성을 유지하기 위해 실제로는 소스코드를 단순화한 버전이므로 구체적인 메소드를 적어주시면 해당 소스코드도 함께 올려드리겠습니다. 주소. 전체 코드는 다음 파일에 있습니다: https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js🎜
Node.js 모듈 로딩 기능은 모두 Module
클래스에 있습니다. 전체 코드는 객체 지향적 사고(JS 객체 지향에 익숙하지 않다면 먼저 이 글을 읽어보세요. Module
클래스의 생성자도 복잡하지 않습니다. 주로 일부 값을 초기화합니다. 공식 Module
이름과 구별하기 위해 자체 클래스 이름을 로 지정합니다. >내모듈< /code>:🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false">function MyModule(id = &#39;&#39;) {
this.id = id; // 这个id其实就是我们require的路径
this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
this.exports = {}; // 导出的东西放这里,初始化为空对象
this.filename = null; // 模块对应的文件名
this.loaded = false; // loaded用来标识当前模块是否已经加载
}</pre><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div><h3 id="item-4-7">require方法</h3><p>我们一直用的<code>require
其实是Module
类的一个实例方法,内容很简单,先做一些参数检查,然后调用Module._load
方法,源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L970。精简版的代码如下:
MyModule.prototype.require = function (id) { return Module._load(id); }
MyModule._load
是一个静态方法,这才是require
方法的真正主体,他干的事情其实是:
- 先检查请求的模块在缓存中是否已经存在了,如果存在了直接返回缓存模块的
exports
。- 如果不在缓存中,就
new
一个Module
实例,用这个实例加载对应的模块,并返回模块的exports
。
我们自己来实现下这两个需求,缓存直接放在Module._cache
这个静态变量上,这个变量官方初始化使用的是Object.create(null)
,这样可以使创建出来的原型指向null
,我们也这样做吧:
MyModule._cache = Object.create(null); MyModule._load = function (request) { // request是我们传入的路劲参数 const filename = MyModule._resolveFilename(request); // 先检查缓存,如果缓存存在且已经加载,直接返回缓存 const cachedModule = MyModule._cache[filename]; if (cachedModule !== undefined) { return cachedModule.exports; } // 如果缓存不存在,我们就加载这个模块 // 加载前先new一个MyModule实例,然后调用实例方法load来加载 // 加载完成直接返回module.exports const module = new MyModule(filename); // load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整 MyModule._cache[filename] = module; module.load(filename); return module.exports; }
上述代码对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L735
可以看到上述源码还调用了两个方法:MyModule._resolveFilename
和MyModule.prototype.load
,下面我们来实现下这两个方法。
MyModule._resolveFilename
MyModule._resolveFilename
从名字就可以看出来,这个方法是通过用户传入的require
参数来解析到真正的文件地址的,源码中这个方法比较复杂,因为按照前面讲的,他要支持多种参数:内置模块,相对路径,绝对路径,文件夹和第三方模块等等,如果是文件夹或者第三方模块还要解析里面的package.json
和index.js
。我们这里主要讲原理,所以我们就只实现通过相对路径和绝对路径来查找文件,并支持自动添加js
和json
两种后缀名:
MyModule._resolveFilename = function (request) { const filename = path.resolve(request); // 获取传入参数对应的绝对路径 const extname = path.extname(request); // 获取文件后缀名 // 如果没有文件后缀名,尝试添加.js和.json if (!extname) { const exts = Object.keys(MyModule._extensions); for (let i = 0; i < exts.length; i++) { const currentPath = `${filename}${exts[i]}`; // 如果拼接后的文件存在,返回拼接的路径 if (fs.existsSync(currentPath)) { return currentPath; } } } return filename; }
上述源码中我们还用到了一个静态变量MyModule._extensions
,这个变量是用来存各种文件对应的处理方法的,我们后面会实现他。
MyModule._resolveFilename
对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L822
MyModule.prototype.load
MyModule.prototype.load
是一个实例方法,这个方法就是真正用来加载模块的方法,这其实也是不同类型文件加载的一个入口,不同类型的文件会对应MyModule._extensions
里面的一个方法:
MyModule.prototype.load = function (filename) { // 获取文件后缀名 const extname = path.extname(filename); // 调用后缀名对应的处理函数来处理 MyModule._extensions[extname](this, filename); this.loaded = true; }
注意这段代码里面的this
指向的是module
实例,因为他是一个实例方法。对应的源码看这里: https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L942
前面我们说过不同文件类型的处理方法都挂载在MyModule._extensions
上面的,我们先来实现.js
类型文件的加载:
MyModule._extensions['.js'] = function (module, filename) { const content = fs.readFileSync(filename, 'utf8'); module._compile(content, filename); }
可以看到js
的加载方法很简单,只是把文件内容读出来,然后调了另外一个实例方法_compile
来执行他。对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L1098
MyModule.prototype._compile
是加载JS文件的核心所在,也是我们最常使用的方法,这个方法需要将目标文件拿出来执行一遍,执行之前需要将它整个代码包裹一层,以便注入exports, require, module, __dirname, __filename
,这也是我们能在JS文件里面直接使用这几个变量的原因。要实现这种注入也不难,假如我们require
的文件是一个简单的Hello World
,长这样:
module.exports = "hello world";
那我们怎么来给他注入module
这个变量呢?答案是执行的时候在他外面再加一层函数,使他变成这样:
function (module) { // 注入module变量,其实几个变量同理 module.exports = "hello world"; }
所以我们如果将文件内容作为一个字符串的话,为了让他能够变成上面这样,我们需要再给他拼接上开头和结尾,我们直接将开头和结尾放在一个数组里面:
MyModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
注意我们拼接的开头和结尾多了一个()
包裹,这样我们后面可以拿到这个匿名函数,在后面再加一个()
就可以传参数执行了。然后将需要执行的函数拼接到这个方法中间:
MyModule.wrap = function (script) { return MyModule.wrapper[0] + script + MyModule.wrapper[1]; };
这样通过MyModule.wrap
包装的代码就可以获取到exports, require, module, __filename, __dirname
这几个变量了。知道了这些就可以来写MyModule.prototype._compile
了:
MyModule.prototype._compile = function (content, filename) { const wrapper = Module.wrap(content); // 获取包装后函数体 // vm是nodejs的虚拟机沙盒模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数 // 返回值就是转化后的函数,所以compiledWrapper是一个函数 const compiledWrapper = vm.runInThisContext(wrapper, { filename, lineOffset: 0, displayErrors: true, }); // 准备exports, require, module, __filename, __dirname这几个参数 // exports可以直接用module.exports,即this.exports // require官方源码中还包装了一层,其实最后调用的还是this.require // module不用说,就是this了 // __filename直接用传进来的filename参数了 // __dirname需要通过filename获取下 const dirname = path.dirname(filename); compiledWrapper.call(this.exports, this.exports, this.require, this, filename, dirname); }
上述代码要注意我们注入进去的几个参数和通过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: 文件所在文件夹的绝对路径。
到这里,我们的JS文件其实已经记载完了,对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L1043
加载json
文件就简单多了,只需要将文件读出来解析成json
就行了:
MyModule._extensions['.json'] = function (module, filename) { const content = fs.readFileSync(filename, 'utf8'); module.exports = JSONParse(content); }
exports
和module.exports
的区别网上经常有人问,node.js
里面的exports
和module.exports
到底有什么区别,其实前面我们的手写代码已经给出答案了,我们这里再就这个问题详细讲解下。exports
和module.exports
这两个变量都是通过下面这行代码注入的。
compiledWrapper.call(this.exports, this.exports, this.require, this, filename, dirname);
初始状态下,exports === module.exports === {}
,exports
是module.exports
的一个引用,如果你一直是这样使用的:
exports.a = 1; module.exports.b = 2; console.log(exports === module.exports); // true
上述代码中,exports
和module.exports
都是指向同一个对象{}
,你往这个对象上添加属性并没有改变这个对象本身的引用地址,所以exports === module.exports
一直成立。
但是如果你哪天这样使用了:
exports = { a: 1 }
或者这样使用了:
module.exports = { b: 2 }
那其实你是给exports
或者module.exports
重新赋值了,改变了他们的引用地址,那这两个属性的连接就断开了,他们就不再相等了。需要注意的是,你对module.exports
的重新赋值会作为模块的导出内容,但是你对exports
的重新赋值并不能改变模块导出内容,只是改变了exports
这个变量而已,因为模块始终是module
,导出内容是module.exports
。
Node.js对于循环引用是进行了处理的,下面是官方例子:
a.js
:
console.log('a 开始'); exports.done = false; const b = require('./b.js'); console.log('在 a 中,b.done = %j', b.done); exports.done = true; console.log('a 结束');
b.js
:
console.log('b 开始'); exports.done = false; const a = require('./a.js'); console.log('在 b 中,a.done = %j', a.done); exports.done = true; console.log('b 结束');
main.js
:
console.log('main 开始'); const a = require('./a.js'); const b = require('./b.js'); console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
当 main.js
加载 a.js
时, a.js
又加载 b.js
。 此时, b.js
会尝试去加载 a.js
。 为了防止无限的循环,会返回一个 a.js
的 exports
对象的 未完成的副本 给 b.js
模块。 然后 b.js
完成加载,并将 exports
对象提供给 a.js
模块。
那么这个效果是怎么实现的呢?答案就在我们的MyModule._load
源码里面,注意这两行代码的顺序:
MyModule._cache[filename] = module; module.load(filename);
上述代码中我们是先将缓存设置了,然后再执行的真正的load
,顺着这个思路我能来理一下这里的加载流程:
main
加载a
,a
在真正加载前先去缓存中占一个位置a
在正式加载时加载了b
b
又去加载了a
,这时候缓存中已经有a
了,所以直接返回a.exports
,即使这时候的exports
是不完整的。
require
不是黑魔法,整个Node.js的模块加载机制都是JS
实现的。exports, require, module, __filename, __dirname
五个参数都不是全局变量,而是模块加载的时候注入的。vm
来实现。this, exports, module.exports
都指向同一个对象,如果你对他们重新赋值,这种连接就断了。module.exports
的重新赋值会作为模块的导出内容,但是你对exports
的重新赋值并不能改变模块导出内容,只是改变了exports
这个变量而已,因为模块始终是module
,导出内容是module.exports
。exports
。本文完整代码已上传GitHub:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/Node.js/Module/MyModule/index.js
参考资料
Node.js模块加载源码:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js
Node.js模块官方文档:http://nodejs.cn/api/modules.html
文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。
作者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges
更多编程相关知识,可访问:编程教学!!
위 내용은 Node.js의 모듈 로딩 메커니즘에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!