淺談模塊化加載的實現原理

試發一彈,本文同步自:http://barretlee.com
略蛋疼的是不支持: [title][url reference]javascript

相信不少人都用過 seajs、 requirejs 等這些模塊加載器,他們都是十分便捷的工程管理工具,簡化了代碼的結構,更重要的是消除了各類文件依賴和命名衝突問題,並利用 AMD / CMD 規範統一了格式。若是你不太明白模塊化的做用,建議看看玉伯寫的一篇文章css

爲何他們會想到使用模塊化加載呢,我以爲主要是兩點。html

  • 一是按需加載,業務愈來愈大,基礎代碼也會愈來愈多,開發人員可能開發了一百個小工具,並且都塞在一個叫作 utils.js 的包裏,可是一個頁面可能只須要三到五個小工具,若是直接去加載這個 utils.js 豈不是很大的浪費,PC 端還好,主要是無線端,剩下 1KB 那都是很大的價值啊,因此呢,現在不少框架的開發都體現出細顆粒度的分化,像百度研究比較賣力的 tangram,阿里放滿產品線的 kissy,幾乎是細分到了微粒程度,這種細分方式也促進了模塊化加載技術的發展,好比爲了減小請求數量,kissy 的 config 中開啓 combo 就能夠合併多個請求爲一個等等。java

  • 第二點,應該也是從服務器那邊參考而來的,服務器腳本不少都是以文件爲單位分離的,若是要利用其它文件的功能,能夠垂手可得的 require 或者 include 進來,我沒有去研究這些加載函數的內部實現原理,稍微猜猜應該是把文件寫入到緩存,遇到 include 之類的加載函數,暫停寫入,找到須要 include 的文件地址,把找到的文件接着上面繼續寫入緩存,以此類推,直到結束,而後編譯器進行統一編譯。git

1、模塊化加載的技術原理

先不考慮各類模塊定義規範,本文目的只是簡要的分析加載原理, CMD / AMD 規範雖內容然很少,可是要實現起來,工程量仍是不小。文章後面會提到。github

1. 數據模塊的加載

既然是模塊化加載,想辦法把模塊內容拿到固然是重頭戲,不管是 script 仍是 css 文件的加載,一個 script 或者 link 標籤就能夠搞定問題,不過我這裏採用的是 ajax,目的是爲了拿到 script 的代碼,也是爲了照顧後面要說的 CMD 規範。ajax

var require = function(path){
    var xhr = new XMLHttpRequest(), res;
    xhr.open("GET", path, true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            // 獲取源碼
            res = xhr.responseText;
        }
    }
    xhr.send();
};

建立 script 便籤加載腳本不會存在跨域問題,不過拿到的腳本會被瀏覽器立馬解析出來,若是要作同異步的處理就比較麻煩了。沒有跨域的文件咱們就經過上面的方式加載,若是腳本跨域了,再去建立標籤,讓文檔本身去加載。正則表達式

// 跨域處理
if(crossDomain){
    var script = document.createElement("script");
    script.src = path;

    (document.getElementsByTagName("head")[0] || document.body).appendChild(script);
}

2. 解析模塊的層次依賴關係

模塊之間存在依賴關係是十分正常的,如一個工程的文件結構以下:跨域

project/
├── css/
│   └── main.css
├── js/
│   ├── require.js
│   └── modlues/
│       ├── a.js
│       ├── b.js
│       └── c.js
└── index.html

而這裏幾個模塊的依賴關係是:數組

┌> a.js -> b.js
index.html -|
            └> c.js

// a.js
require("./js/test/b.js");

// b.js
console.log("i am b");

// c.js
console.log("i am c");

咱們要從 index.html 中利用 require.js 獲取這一連串的依賴關係,通常採用的方式就是正則匹配。以下:先拿到 function 的代碼,而後正則匹配出第一層的依賴關係,接着加載匹配到關係的代碼,繼續匹配。

// index.html
<script type="text/javascript" src="./js/require.js"></script>
<script type="text/javascript">
    function test(){
        var a = require("./js/modlues/a.js");
        var c = require("./js/modlues/c.js");
    }

    // toString 方法能夠拿到 test 函數的 code
    start(test.toString());
</script>

整個函數的入口是 start,正則表達式爲:

var r = /require\((.*)\)/g;

var start = function(str){
    while(match = r.exec(str)) {
        console.log(match[1]);
    }
};

由此咱們拿到了第一層的依賴關係,

["./js/modlues/a.js", "./js/modlues/c.js"]

接着要拿到 a.js 和 b.js 的文件層次依賴,以前咱們寫了一個 require 函數,這個函數能夠拿到腳本的代碼內容,不過這個 require 函數要稍微修改下,遞歸去查詢和下載代碼。

var cache = {};
var start = function(str){
    while(match = r.exec(str)) {
        console.log(match && match[1]);
        // 若是匹配到了內容,下載 path 對應的源碼
        match && match[1] && require(match[1]);
    }
};

var require = function(path){
    var xhr = new XMLHttpRequest(), res;
    xhr.open("GET", path, true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            res = xhr.responseText;
            // 緩存文件
            cache[path] = res;
            // 繼續遞歸匹配
            start(res);
        }
    }
    xhr.send();
};

上面的代碼已經能夠很好地拿到文件遞歸關係了。

3. 添加事件機制,優化管理代碼

可是咱們有必要先把 responseText 緩存起來,若是不緩存文件,直接 eval 獲得的 responseText 代碼,想一想會發生什麼問題~ 若是模塊之間存在循環引用,如:

┌> a.js -> b.js
index.html -|
            └> b.js -> a.js

那 start 和 require 將會陷入死循環,不斷的加載代碼。因此咱們須要先拿到依賴關係,而後解構關係,分析出咱們須要加載哪些模塊。值得注意的是,咱們必須按照加載的順序去 eval 代碼,若是 a 依賴 b,先去執行 a 的話,必定會報錯!

有兩個問題我糾結了半天,上面的請求方式,什麼時候會結束?用什麼方式去記錄文件依賴關係?

最後仍是決定將 start 和 require 兩個函數的相互遞歸修改爲一個函數的遞歸。用一個對象,發起請求時把 URL 做爲 key,在這個對象裏保存 XHR 對象,XHR 對象請求完成後,把抓取到的新請求再用一樣的方式放入這個對象中,同時從這個對象中把本身刪除掉,而後判斷這個對象上是否存在 key, 若是存在說明還有 XHR 對象沒完成。

var r = /require\(\s*"(.*)"\s*\)/g;
var cache = {};    // 文件緩存
var relation = []; // 依賴過程控制
var obj = {};      // xhr 管理對象

//輔助函數,獲取鍵值數組
Object.keys = Object.keys || function(obj){
    var a = [];
    for(a[a.length] in obj);
    return a ;
};

// 入口函數
function start(str){
    while(match = r.exec(str)){
        obj[match[1]] = new XMLHttpRequest();
        require(obj[match[1]], match[1]);
    }
}

// 遞歸請求
var require = function(xhr, path){
    //記錄依賴過程
    relation.push(path);

    xhr.open("GET", path, true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            var res = xhr.responseText;
            // 緩存文件
            cache[path] = res;
            // 從xhr對象管理器中刪除已經加載完畢的函數
            delete obj[path];

            // 若是obj爲空則觸發 allLoad 事件
            Object.keys(obj).length == 0 ? Event.trigger("allLoad") : void 0;
            //遞歸條件
            while(match = r.exec(res)){
                obj[match[1]] = new XMLHttpRequest();
                require(obj[match[1]], match[1]);
            }
        }
    }
    xhr.send();
};

上面的代碼已經基本完成了文件依賴分析,文件的加載和緩存工做了,我寫了一個,有興趣能夠看一看。這個demo的文件結構爲:

project/
├── js/
│   ├── require.js
│   └── test/
│       ├── a.js
│       ├── b.js
│       ├── c.js
│       ├── d.js
│       └── e.js
└── index.html

//文件依賴關係爲
                       ┌> c.js
            ┌> a.js ->-|
index.html -|          └> d.js
            └> b.js -> e.js

戳我 → Demo

4. CMD 規範的介紹

上面寫了一大堆內容,也實現了模塊加載器的原型,可是放在實際應用中,他就是個廢品,回到最開始,咱們爲何要使用模塊化加載。目的是爲了避免去使用麻煩的命名空間,把複雜的模塊依賴交給 require 這個函數去管理,但實際上呢,上面拿到的全部模塊都是暴露在全局變量中的,也就是說,若是 a.js 和 b.js 中存在命名相同的變量,後者將會覆蓋前者,這是咱們不肯意看到的。爲了處理此類問題,咱們有必要把全部的模塊都放到一個閉包中,這樣一來,只要不使用 window.vars 命名,閉包之間的變量是不會相互影響的。咱們可使用本身的方式去管理代碼,不過有人已經研究處理一套標準,並且是全球統一,那就拿着用吧~

關於 CMD 規範,我這裏就很少說了,能夠去看看草案,玉伯也翻譯了一份,。每一模塊有且僅有一個對外公開的接口 exports,如:

define(function(require, exports) {

  // 對外提供 foo 屬性
  exports.foo = 'bar';

  // 對外提供 doSomething 方法
  exports.doSomething = function() {};

});

剩下的工做就是針對 CMD 規範寫一套符合標準的代碼接口,這個比較瑣碎,就不寫了。

2、額外的話題

上面的代碼中提到了關於 Event 的事件管理。在模塊所有加在完畢以後,須要有個東西告訴你,因此順手寫了一個 Event 的事件管理器。

// Event
var Event = {};
Event.events = [];
Event.on = function(evt, func){
    for(var i = 0; i < Event.events.length; i++){
        if(Event.events[i].evt == evt){
            Event.events[i].func.push(func);
            return;
        }
    }

    Event.events.push({
        evt: evt,
        func: [func]
    });
};
Event.trigger = function(evt){
    for(var i = 0; i < Event.events.length; i++){
        if(Event.events[i].evt == evt){
            for(var j = 0; j < Event.events[i].func.length; j++){
                Event.events[i].func[j]();
            }
            return;
        }
    }
};
Event.off = function(evt){
    for(var i = 0; i < Event.events.length; i++){
        Event.events.splice(i, 1);
    }       
};

我以爲 seajs 是一個很不錯的模塊加載器,若是感興趣,能夠去看看他的源碼實現,代碼不長,只有一千多行。模塊的加載它採用的是建立文本節點,讓文檔去加載模塊,實時查看狀態爲 interactive 的 script 標籤,若是處於交互狀態就拿到他的代碼,接着刪除節點。當節點數目爲 0 的時候,加載工做完成。

本文沒有考慮 css 文件的加載問題,咱們能夠把它當作一個沒有 require 關鍵詞的 js 文件,或者把它匹配出來以後另做處理,由於他是不可能存在模塊依賴關係的。

而後就是不少不少細節,本文的目的並非寫一個相似 seajs 的模塊管理工具,只是稍微說幾句本身對這玩意兒的見解,若是說的有錯,請多多吐槽!

3、參考資料

相關文章
相關標籤/搜索