How 模块?
在前文的 What?Why?之后就是我们最直接的如何实现我们的模块编译器了。
在 Nodejs 中其自带的模块化方法 require
是最常见的一种引入其他模块的方式。我们来简单回顾一下 Nodejs 中如何使用require
来引入其他模块。
题目:实现一个简单的缓存模块cache
,实现 get
和 set
方法来获取和设置key
:value
键值对。
实现功能需要两个文件:
cache
├── cache.js
└── index.js
- 缓存模块文件
cache.js
,用于实现缓存方法:
/**
* cache/cache.js
*/
class Cache {
constructor() {
this.cache = {};
}
get(key) {
return this.cache[key];
}
set(key, value) {
return (this.cache[key] = value);
}
}
module.exports = new Cache(); // 导出一个实例化的 Cache 对象。
- 入口文件:
index.js
,用于描述如何使用缓存模块:
/**
* cache/index.js
*/
const cache = require('./cache'); // 导入 Cache 模块中实例化的 Cache 对象。
cache.set('name', 'Herb');
console.log(cache.get('name')); // Herb
到现在为止都是使用了标准的 Nodejs 模块函数require
。那么如果仅仅是知道require
是怎么用的其实只是满足了一个初级程序员的需求。那么从想完成从初级->高级这一跨越,就至少要了解require
的机制,Nodejs 如何通过require
来完成不同模块之间的引用的呢???
1. 探寻require
的工作原理
当我们遇到问题需要分析一些工具的底层原理的时候,往往会走两条路:
- 在 Google/Baidu 上搜索相关原理分析文章,看别人理解后提炼的内容
- 阅读工具的官方文档甚至源码
这两条路没有优劣之分,也不要纠结哪种方式更好。我认为这两种方式是属于不同层次的,第一种看别人理解后的内容是一条捷径,往往是从更高抽象层次去理解其原理。第二种应该是建立在对原理有大致了解之后去细化的一个过程。
这里作者就不分析源码了,把require
最精华的部分梳理出来,帮助我们做出 MVP(Minimum Viable Product)就可以。
如果我们仔细观察现有的代码,会注意到:
module.exports
是一个对象,代表了从当前模块要导出的对象。require
是一个函数,函数的返回值是其模块导出的对象。- 未被导出的对象是无法被其他模块使用的。
module
和require
是直接使用的,并不知道其在哪里声明的。
从上面几个特征可以得出其重要的两个点:
module
顾名思义代表了当前模块,module
有一个exports
属性用于存放当前模块要导出的对象。所以module
是一个对象。require
方法会根据文件路径寻找到其对应的模块,并返回其模块导出的对象module.exports
。
2. 声明一个模块
从上面require
的原理中可以得出一个模块module
有一个重要属性exports
,还有require
是根据文件路径寻找到这个模块的,最容易想到的是每个模块可以通过其文件路径唯一标识它。那么一个模块至少就拥有两个属性:
id
: 使用文件路径代表exports
: 模块要导出的对象
├── cache
│ ├── cache.js
│ └── index.js
├── module.js
/**
* module.js
*/
class Module {
constructor(id, exports = {}) {
this.id = id;
this.exports = exports;
}
}
3. 为cache.js
创建一个模块
通过阅读 Nodejs 官方文档关于 module 部分的解释时可以发现,每个模块都是被一个函数包裹着的: nodejs.org/api/modules…
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});
那么这就解释了在分析require
原理的时候的一个疑惑,require
和module
的来源问题。其实就是包裹模块的函数的两个参数。
同样的我们可以简单实现最基础的模块,只需要包裹模块的函数有两个参数就足够了require
和module
,为了更清晰的展示其工作原理,我们将cache.js
的文件内容拷贝直接拷贝到module.js
中,并包裹在一个函数里:
/**
* module.js
*/
class Module {
constructor(id, exports = {}) {
this.id = id;
this.exports = exports;
}
}
function cache(module, require) {
/**
* cache/cache.js
*/
class Cache {
constructor() {
this.cache = {};
}
get(key) {
return this.cache[key];
}
set(key, value) {
return (this.cache[key] = value);
}
}
module.exports = new Cache(); // 导出一个实例化的 Cache 对象。
}
那么如何才能创建并导出正确的cache
模块呢?
首先是先创建出我们的cacheModule
模块对象:
const cacheModule = new Module('./cache');
此时我们给模块的id
暂时设为./cache
,并且exports
初始为一个空对象{}
然后调用cache
方法来加载我们的模块,
在模块内部暂时并没有使用require
方法,所以先忽略掉它:
cache(cacheModule);
很显然运行完cache
方法之后,我们的cacheModule
的exports
属性就会包含我们导出的new Cache()
这一对象了:
console.log(cacheModule); // Module { id: 'cache.js', exports: Cache { cache: {} } }
到此我们完成了cacheModule
的创建工作。
4. 为index.js
创建一个模块
同样的我们以相同的方式包裹index.js
的文件内容,然后加载我们的index.js
模块,唯一不同的是我们需要实现require
方法来引入cache.js
模块。
由于我们每个模块都有一个唯一标识id
,那么我们可以创建一个modules
的对象来索引我们的模块:
const modules = {
'cache.js': cacheModule,
};
那么根据require
函数的需求:
根据模块
id
找到对应模块,并返回模块的导出对象exports
实现一个简单require
函数:
const __require__ = (id) => modules[id].exports; // 避免与 Nodejs 自身的 require 冲突。
包裹并加载我们的index.js
模块:
function index(module, require) {
/**
* cache/index.js
*/
const cache = require('./cache'); // 导入 Cache 模块中实例化的 Cache 对象。
cache.set('name', 'Herb');
console.log(cache.get('name'));
}
const indexModule = new Module('index.js');
index(indexModule, __require__); // Herb
其实index.js
模块就是我们程序的入口文件,加载入口文件就等同于运行了我们的程序。此时我们通过运行node module.js
之后就能看到终端打印出了Herb
。
引用
资源
文章完整源码在我的 github 仓库中: github.com/xahhy/nodej…