NodeJS 模塊化的簡易實現(commonJS)

CommonJS 概述

CommonJS 是一種模塊化的標準,而 NodeJS 是這種標準的實現,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。javascript

NodeJS 模塊化的簡易實現

在實現模塊加載以前,咱們須要清除模塊的加載過程:java

  • 假設 A 文件夾下有一個 a.js,咱們要解析出一個絕對路徑來;
  • 咱們寫的路徑可能沒有後綴名 .js.json
  • 獲得一個真實的加載路徑(模塊會被緩存)先去緩存中看一下這個文件是否存在,若是存在返回緩存 沒有則建立一個模塊;
  • 獲得對應文件的內容,加一個閉包,把內容塞進去,以後執行便可。

一、提早加載須要用到的模塊

由於咱們只是實現 CommonJS 的模塊加載方法,並不會去實現整個 Node,在這裏咱們須要依賴一些 Node 的模塊,因此咱們就 「不要臉」 的使用 Node 自帶的 require 方法把模塊加載進來。json

依賴模塊
1
2
3
4
5
6
7
8
複製代碼
// 操做文件的模塊
const fs = require("fs");

// 處理路徑的模塊
const path = require("path");

// 虛擬機,幫咱們建立一個黑箱執行代碼,防止變量污染
const vm = require("vm");
複製代碼

二、建立 Module 構造函數

其實 CommonJS 中引入的每個模塊咱們都須要經過 Module 構造函數建立一個實例。數組

建立 Module 構造函數
1
2
3
4
5
6
7
8
複製代碼
/* * @param {String} p */
function Module(p) {
    this.id = p; // 當前文件的表示(絕對路徑)
    this.exports = {}; // 每一個模塊都有一個 exports 屬性,用來存儲模塊的內容
    this.loaded = false; // 標記是否被加載過
}
複製代碼

三、定義靜態屬性存儲咱們須要使用的一些值

Module 靜態變量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
複製代碼
// 函數後面須要使用的閉包的字符串
Module.wrapper = [
    "(function (exports, require, module, __dirname, __filename) {",
    "\n})"
];

// 根據絕對路徑進行緩存的模塊的對象
Module._cacheModule = {};

// 處理不一樣文件後綴名的方法
Module._extensions = {
    ".js": function() {},
    ".json": function() {}
};
複製代碼

四、建立引入模塊的 req 方法

爲了防止和 Node 自帶的 require 方法重名,咱們將模擬的方法重命名爲 req緩存

引入模塊方法 req
1
2
3
4
5
6
7
8
9
10
複製代碼
/* * @param {String} moduleId */
function req(moduleId) {
    // 將 req 傳入的參數處理成絕對路徑
    let p = Module._resolveFileName(moduleId);

    // 生成一個新的模塊
    let module = new Module(p);
}
複製代碼

在上面代碼中,咱們先把傳入的參數經過 Module._resolveFileName 處理成了一個絕對路徑,並建立模塊實例把絕對路徑做爲參數傳入,咱們如今實現一下 Module._resolveFileName 方法。bash

五、返回文件絕對路徑 Module._resolveFileName 方法的實現

這個方法的功能就是將 req 方法的參數根據是否有後綴名兩種方式處理成帶後綴名的文件絕對路徑,若是 req的參數沒有後綴名,會去按照 Module._extensions 的鍵的後綴名順序進行查找文件,直到找到後綴名對應文件的絕對路徑,優先 .js,而後是 .json,這裏咱們只實現這兩種文件類型的處理。閉包

處理絕對路徑 _resolveFileName 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
複製代碼
/* * @param {String} moduleId */
Module._resolveFileName = function(moduleId) {
    // 將參數拼接成絕對路徑
    let p = path.resolve(moduleId);

    // 判斷是否含有後綴名
    if (!/\.\w+$/.test(p)) {
        // 建立規範規定查找文件後綴名順序的數組 .js .json
        let arr = Object.keys(Module._extensions);

        // 循環查找
        for (let i = 0; i < arr.length; i++) {
            // 將絕對路徑與後綴名進行拼接
            let file = p + arr[i];
            // 查找不到文件時捕獲異常
            try {
                // 並經過 fs 模塊同步查找文件的方法對改路徑進行查找,文件未找到會直接進入 catch 語句
                fs.accessSync(file);

                // 若是找到文件將該文件絕對路徑返回
                return file;
            } catch (e) {
                // 當後綴名循環完畢都沒有找到對應文件時,拋出異常
                if (i >= arr.length) throw new Error("not found module");
            }
        }
    } else {
        // 有後綴名直接返回該絕對路徑
        return p;
    }
};
複製代碼

六、加載模塊的 load 方法

完善 req 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
複製代碼
/* * @param {String} moduleId */
function req(moduleId) {
    // 將 req 傳入的參數處理成絕對路徑
    let p = Module._resolveFileName(moduleId);

    // 生成一個新的模塊
    let module = new Module(p);

    // ********** 下面爲新增代碼 **********
    // 加載模塊
    let content = module.load(p);

    // 將加載後返回的內容賦值給模塊實例的 exports 屬性上
    module.exports = content;

    // 最後返回 模塊實例的 exports 屬性,即加載模塊的內容
    return module.exports;
    // ********** 上面爲新增代碼 **********
}
複製代碼

上面代碼實現了一個實例方法 load,傳入文件的絕對路徑,爲模塊加載文件的內容,在加載後將值存入模塊實例的 exports 屬性上最後返回,其實 req 函數返回的就是模塊加載回來的內容。app

load 方法
1
2
3
4
5
6
7
8
9
10
11
複製代碼
// 模塊加載的方法
Module.prototype.load = function(filepath) {
    // 判斷加載的文件是什麼後綴名
    let ext = path.extname(filepath);

    // 根據不一樣的後綴名處理文件內容,參數是當前實例
    let content = Moudule._extensions[ext](this);

    // 將處理後的結果返回
    return content;
};
複製代碼

七、實現加載 .js 文件和 .json 文件的方法

還記得前面準備的靜態屬性中有 Module._extensions 就是用來存儲這兩個方法的,下面咱們來完善這兩個方法。svg

處理後綴名方法的 _extensions 對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
複製代碼
Module._extensions = {
    ".js": function(module) {
        // 讀取 js 文件,返回文件的內容
        let script = fs.readFileSync(module.id, "utf8");

        // 給 js 文件的內容增長一個閉包環境
        let fn = Module.wrap(script);

        // 建立虛擬機,將咱們建立的 js 函數執行,將 this 指向模塊實例的 exports 屬性
        vm.runInThisContext(fn).call(
            module.exports,
            module.exports,
            req,
            module
        );

        // 返回模塊實例上的 exports 屬性(即模塊的內容)
        return module.exports;
    },
    ".json": function(module) {
        // .json 文件的處理相對簡單,將讀出的字符串轉換成對象便可
        return JSON.parse(fs.readFileSync(module.id, "utf8"));
    }
};
複製代碼

咱們這裏使用了 Module.wrap 方法,代碼以下,其實幫助咱們加了一個閉包環境(即套了一層函數並傳入了咱們須要的參數),裏面全部的變量都是私有的。模塊化

建立閉包 wrap 方法
1
2
3
複製代碼
Module.wrap = function(content) {
    return Module.wrapper[0] + content + Module.wrapper[1];
};
複製代碼

Module.wrapper 的兩個值其實就是咱們須要在外層包了一個函數的前半段和後半段。

這裏咱們要劃重點了,很是重要:
一、咱們在虛擬機中執行構建的閉包函數時利用執行上/下文 callthis 指向了模塊實例的 exports屬性上,因此這也是爲何咱們用 Node 啓動一個 js 文件,打印 this 時,不是全局對象 global,而是一個空對象,這個空對象就是咱們的 module.exports,即當前模塊實例的 exports 屬性。
二、仍是第一條的函數執行,咱們傳入的第一個參數是改變 this 指向,那第二個參數是 module.exports,因此在每一個模塊導出的時候,使用 module.exports = xxx,其實直接替換了模塊實例的值,即直接把模塊的內容存放在了模塊實例的 exports 屬性上,而 req 最後返回的就是咱們模塊導出的內容。
三、第三個參數之因此傳入 req 是由於咱們還可能在一個模塊中導入其餘模塊,而 req 會返回其餘模塊的導出在當前模塊使用,這樣整個 CommonJS 的規則就這樣創建起來了。

八、對加載過的模塊進行緩存

咱們如今的程序是有問題的,當重複加載了一個已經加載過得模塊,當執行 req 方法的時候會發現,又建立了一個新的模塊實例,這是不合理的,因此咱們下面來實現一下緩存機制。

還記得以前的一個靜態屬性 Module._cacheModule,它的值是一個空對象,咱們會把全部加載過的模塊的實例存儲到這個對象上。

完善 req 方法(處理緩存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
複製代碼
/* * @param {String} moduleId */
function req(moduleId) {
    // 將 req 傳入的參數處理成絕對路徑
    let p = Module._resolveFileName(moduleId);

    // ********** 下面爲新增代碼 **********
    // 判斷是否已經加載過
    if (Module._cacheModule[p]) {
        // 模塊存在,若是有直接把 exports 對象返回便可
        return Module._cacheModule[p].exprots;
    }
    // ********** 上面爲新增代碼 **********

    // 生成一個新的模塊
    let module = new Module(p);

    // 加載模塊
    let content = module.load(p);

    // ********** 下面爲新增代碼 **********
    // 存儲時是拿模塊的絕對路徑做爲鍵與模塊內容相對應的
    Module._cacheModule[p] = module;

    // 是否緩存表示改成 true
    module.loaded = true;
    // ********** 上面爲新增代碼 **********

    // 將加載後返回的內容賦值給模塊實例的 exports 屬性上
    module.exports = content;

    // 最後返回 模塊實例的 exports 屬性,即加載模塊的內容
    return module.exports;
}
複製代碼

九、試用 req 加載模塊

在同級目錄下新建一個文件 a.js,使用 module.exports 隨便導出一些內容,在咱們實現模塊加載的最下方嘗試引入並打印內容。

導出自定義模塊
1
2
複製代碼
// a.js
module.exports = "Hello world";
複製代碼
檢測 req 方法
1
2
複製代碼
const a = req("./a");
console.log(a); // Hello world
複製代碼

CommonJS 模塊查找規範

其實咱們只實現了 CommonJS 規範的一部分,即自定義模塊的加載,其實在 CommonJS 的規範當中關於模塊查找的規則還有不少,具體的咱們就用下面的流程圖來表示。


CommonJS模塊加載流程圖

這篇文章讓咱們瞭解了 CommonJS 是什麼,主要目的在於理解 Node 模塊化的實現思路,想要更深刻的瞭解 CommonJS 的實現細節,建議看一看 NodeJS 源碼對應的部分,若是以爲源碼比較多,不容易找到模塊化實現的代碼,也能夠在 VSCode 中經過調用 require 方法引入模塊時,打斷點調試,一步一步的跟進到 Node 源碼中查看。

原文出自:https://www.pandashen.com

相關文章
相關標籤/搜索