從零開始構建實現一個 JavaScript 模塊化加載器

jopen 9年前發布 | 26K 次閱讀 JavaScript開發 JavaScript

對任何程序,都存在一個規模的問題,起初我們使用函數來組織不同的模塊,但是隨著應用規模的不斷變大,簡單的重構函數并不能順利的解決問題。尤其對JavaScript程序而言,模塊化有助于解決我們在前端開發中面臨的越來越復雜的需求。

為什么需要模塊化

對開發者而言,有很多理由去將程序拆分為小的代碼塊。這種模塊拆分的過程有助于開發者更清晰的閱讀和編寫代碼,并且能夠讓編程的過程更多的集中在模塊的功能實現上,和算法一樣,分而治之的思想有助于提高編程生產率。

在本文中,我們將集中討論JavaScript的模塊化開發,并實現一個簡單的module loader。對于模塊化的基礎知識,可以參考閱讀 這篇文章

實現模塊化

使用函數作為命名空間

在JavaScript中,函數是唯一的可以用來創建新的作用域的途徑。考慮到一個最簡單的需求,我們通過數字來獲得星期值,例如通過數字0得到星期日,通過數字1得到星期一。我們可以編寫如下的程序:

var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

function dayName(number) { return names[number]; }

console.log(dayName(1));</pre>

上面的程序,我們創建了一個函數dayName() 來獲取星期值。但問題是,names 變量被暴露在全局作用域中。更多的時候,我們希望能夠構造私有變量,而暴露公共函數作為接口。

對JavaScript中的函數而言,我們可以通過創建立即調用的函數表達式來達到這個效果,我們可以通過如下的方式重構上面的代碼,使得私有作用域成為可能:

var dayName = function() {
    var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
    return {
        name: function(number) {
            return names[number];
        },
        number: function(name) {
            return names.indexOf(name);
        }
    };
}();
console.log(dayName.name(3));
console.log(dayName.number("Sunday"));

上面的程序中,我們通過將變量包括在一個函數中,這個函數會立即執行,并返回一個包含兩個屬性的對象,返回的對象會被賦值給dayName 變量。在后面,我們可以通過dayName 變量來訪問暴露的兩個函數接口name 和number 。

對代碼進一步改進,我們可以利用一個exports 對象來達到暴露公共接口的目的,這種方法可以通過如下方法實現,代碼如下:

var weekDay = {};
(function(exports) {
    var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
    exports.name = function(number) {
        return names[number];
    };
    exports.number = function(name) {
        return names.indexOf(name);
    };
})(weekDay); // outside of a function, this refers to the global scope object
console.log(weekDay.name(weekDay.number("Saturday")));

上面的這種模塊構造方式在以瀏覽器為核心的前端編碼中非常常見,通過暴露一個全局變量的方式來將代碼包裹在私有的函數作用域中。但這種方法依然會存在問題,在復雜應用中,你依然無法避免同名變量。

從全局作用域中分離,實現require 方法

更進一步的,為了實現模塊化,我們可以通過構造一個系統,使得一個函數可以require 另一個函數的方式來實現模塊化編程。所以我們的目標是,實現一個require 方法,通過傳入模塊名來取得該模塊的調用。這種實現方式要比前面的方法更為優雅的體現模塊化的理念。對require 方法而言,我們需要完成兩件事。

  1. 我們需要實現一個readFile 方法,它能通過給定字符串返回文件的內容。
  2. 我們需要能夠將返回的字符串作為代碼進行執行。
  3. </ol>

    我們假設已經存在了readFile 這個方法,我們更加關注的是如何能夠將字符串作為可執行的程序代碼。通常我們有好幾種方法來實現這個需求,最常見的方法是eval 操作符,但我們常常在剛學習JavaScript的時候被告知,使用eval 是一個非常不明智的決策,因為使用它會導致潛在的安全問題,因此我們放棄這個方法。

    一個更好的方法是使用Function 構造器,它需要兩個參數:使用逗號分隔的參數列表字符串,和函數體字符串。例如:

    var plusOne = new Function("n", "return n+1");
    console.log(plusOne(5)); // 6

    下面我們可以來實現require 方法了:

    // module.js
    function require(name) {
        // 調用一個模塊,首先檢查這個模塊是否已被調用
        if(name in require.cache) {
            return require.cache[name];
        }
        var code = new Function("exports, module", readFile(name));
        var exports = {},
            module = {exports: exports};
        code(exports, module);
        require.cache[name] = module.exports;
        return module.exports;
    }
    // 緩存對象,為了應對重復調用的問題 
    require.cache = Object.create(null);
    // todo:
    function readFile(fileName) { ... }

    在頁面中使用require 函數:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>demo</title>
        <script src="module.js"></script>
    </head>
    <body>
        <script>
            var weekDay = require("weekDay");
            var today = require("today");
            console.log(weekDay.name(today.dayNumber()));
        </script>
    </body>
    </html>

    通過這種方式實現的模塊化系統通常被稱為是CommonJS模塊風格的,Node.js正式使用了這種風格的模塊化系統。這里只是提供了一個最簡單的實現方法,在真實應用中會有更加精致的實現方法。

    慢載入模塊和AMD

    對瀏覽器編程而言,通常不會使用CommonJS 風格的模塊化系統,因為對于Web而言,加載一個資源遠沒有在服務端來的快,這收到網絡性能的影響,尤其一個模塊如果過大的話,可能會中斷方法的執行。 Browserify 是解決這個問題的一個比較出名的模塊化方案。

    這個過程大概是這樣的:首先檢查模塊中是否存在require 其他模塊的語句,如果有,就解析所有相關的模塊,然后組裝為一個大模塊。網站本身為簡單的加載這個組裝后的大模塊。

    模塊化的另一個解決方案是使用AMD,即異步模塊定義,AMD允許通過異步的方式加載模塊內容,這種就不會阻塞代碼的執行。關于AMD的相關背景知識,可以參考 這篇文章

    我們想要實現的功能大概是這個樣子的:

    // index.html 中的部分代碼
    define(["weekDay.js", "today.js"], function (weekDay, today) {
            console.log(weekDay.name(today.dayNumber()));
            document.write(weekDay.name(today.dayNumber()));
        });

    問題的核心是實現define 方法,它的第一個參數是定義該模塊說需要的依賴列表,參數而是該模塊的具體工作函數。一旦所依賴的模塊都被加載后,define 便會執行參數2所定義的工作函數。weekDay 模塊的內容大概是下面的內容:

    // weekDay.js
    define([], function() {
        var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
        return {
            name: function(number) { return names[number]},
            number: function(name) { return names.indexOf(name)}
        }
    });

    下面我們來關注如何實現define() 方法。為了實現這個方法,我們需要定義一個backgroundReadFile() 方法來異步的獲取文件內容。此外我們需要能夠監視模塊的加載狀態,當模塊加載完后能夠告訴函數去執行具體的工作函數(回調)。

    // 通過Ajax來異步加載模塊
    function backgroundReadFile(url, callback) {
        var req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.addEventListener("load", function () {
            if (req.status < 400)
                callback(req.responseText);
        });
        req.send(null);
    }

    通過實現一個getModule 函數,通過給定的模塊名進行模塊的調度運行工作。同樣,我們需要通過緩存的方式避免同一個模塊被重復的載入。實現代碼如下:

    // module.js 的部分內容
    var defineCache = Object.create(null);
    var currentMod = null;
    function getModule(name) {
        if (name in defineCache) {
            return defineCache[name];
        }
        var module = {
            exports: null,
            loaded: false,
            onLoad: []
        };
        defineCache[name] = module;
        backgroundReadFile(name, function(code) {
            currentMod = module;
            new Function("", code)();
        });
        return module;
    }

    有了getModule() 了函數之后,define 方法可以借助該方法來為當前模塊的依賴列表獲取或創建模塊對象。define 方法的簡單實現如下:

    // module.js 的部分內容
    function define(depNames, moduleFunction) {
        var myMod = currentMod;
        var deps = depNames.map(getModule);
        deps.forEach(function(mod) {
            if(!mod.loaded) {
                mod.onLoad.push(whenDepsLoaded);
            }
        });
        // 用于檢查是否所有的依賴模塊都被成功加載了
        function whenDepsLoaded() {
            if(!deps.every(function(m) { return m.loaded; })) {
                return;
            }
            var args = deps.map(function(m) { return m.exports; });
            var exports = moduleFunction.apply(null, args);
            if (myMod) {
                myMod.exports = exports;
                myMod.loaded = true;
                myMod.onLoad.forEach(function(f) { f(); });
            }
        }
        whenDepsLoaded();
    }

    關于AMD的更加常見的實現是 RequireJS ,它提供了AMD風格的更加流行的實現方式。

    小結

    模塊通過將代碼分離為不同的文件和命名空間,為大型程序提供了清晰的結構。通過構建良好的接口可以使得開發者更加建議的閱讀、使用、擴展代碼。尤其對JavaScript語言而言,由于天生的缺陷,使得模塊化更加有助于程序的組織。在JavaScript的世界,有兩種流行的模塊化實現方式,一種稱為CommonJS,通常是服務端的模塊化實現方案,另一種稱為AMD,通常針對瀏覽器環境。其他關于模塊化的知識,你可以參考 這篇文章

    References

    1. Eloquent JavaScript, chapter 10, Modules
    2. Browserify運行原理分析
    3. Why AMD?
    4. JavaScript模塊化知識點小結
    5. </ol> 原文 http://wwsun.me/posts/creating-javascript-modules-loader.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!