你的第一個 Nodejs 模組編譯器 - (2)
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…