1. 程式人生 > 程式設計 >你的第一個 Nodejs 模組編譯器 - (2)

你的第一個 Nodejs 模組編譯器 - (2)

How 模組?

在前文的 What?Why?之後就是我們最直接的如何實現我們的模組編譯器了。

在 Nodejs 中其自帶的模組化方法 require 是最常見的一種引入其他模組的方式。我們來簡單回顧一下 Nodejs 中如何使用require來引入其他模組。

題目:實現一個簡單的快取模組cache,實現 getset 方法來獲取和設定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是一個函式,函式的返回值是其模組匯出的物件
  • 未被匯出的物件是無法被其他模組使用的。
  • modulerequire是直接使用的,並不知道其在哪裡宣告的。

從上面幾個特徵可以得出其重要的兩個點:

  • 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原理的時候的一個疑惑,requiremodule的來源問題。其實就是包裹模組的函式的兩個引數。

同樣的我們可以簡單實現最基礎的模組,只需要包裹模組的函式有兩個引數就足夠了requiremodule,為了更清晰的展示其工作原理,我們將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方法之後,我們的cacheModuleexports屬性就會包含我們匯出的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…