前端工程-從原理到輪子之JS模組化

網易考拉前端團隊發表於2017-09-21

目前,一個典型的前端專案技術框架的選型主要包括以下三個方面:

  1. JS模組化框架。(Require/Sea/ES6 Module/NEJ)
  2. 前端模板框架。(React/Vue/Regular)
  3. 狀態管理框架。(Flux/Redux)
    系列文章將從上面三個方面來介紹相關原理,並且嘗試自己造一個簡單的輪子。

本篇介紹的是JS模組化
JS模組化是隨著前端技術的發展,前端程式碼爆炸式增長後,工程化所採取的必然措施。目前模組化的思想分為CommonJS、AMD和CMD。有關三者的區別,大家基本都多少有所瞭解,而且資料很多,這裡就不再贅述。

模組化的核心思想:

  1. 拆分。將js程式碼按功能邏輯拆分成多個可複用的js程式碼檔案(模組)。
  2. 載入。如何將模組進行載入執行和輸出。
  3. 注入。能夠將一個js模組的輸出注入到另一個js模組中。
  4. 依賴管理。前端工程模組數量眾多,需要來管理模組之間的依賴關係。

根據上面的核心思想,可以看出要設計一個模組化工具框架的關鍵問題有兩個:一個是如何將一個模組執行並可以將結果輸出注入到另一個模組中;另一個是,在大型專案中模組之間的依賴關係很複雜,如何使模組按正確的依賴順序進行注入,這就是依賴管理。

下面以具體的例子來實現一個簡單的基於瀏覽器端AMD模組化框架(類似NEJ),對外暴露一個define函式,在回撥函式中注入依賴,並返回模組輸出。要實現的如下面程式碼所示。

define([
    '/lib/util.js', //絕對路徑
    './modal/modal.js', //相對路徑
    './modal/modal.html',//文字檔案
], function(Util, Modal, tpl) {
    /*
    * 模組邏輯
    */
    return Module;
})複製程式碼

1. 模組如何載入和執行

先不考慮一個模組的依賴如何處理。假設一個模組的依賴已經注入,那麼如何載入和執行該模組,並輸出呢?
在瀏覽器端,我們可以藉助瀏覽器的script標籤來實現JS模組檔案的引入和執行,對於文字模組檔案則可以直接利用ajax請求實現。
具體步驟如下:

  • 第一步,獲取模組檔案的絕對路徑
    要在瀏覽器內載入檔案,首先要獲得對應模組檔案的完整網路絕對地址。由於a標籤的href屬性總是會返回絕對路徑,也就是說它具有把相對路徑轉成絕對路徑的能力,所以這裡可以利用該特性來獲取模組的絕對網路路徑。需要指出的是,對於使用相對路徑的依賴模組檔案,還需要遞迴先獲取當前模組的網路絕對地址,然後和相對路徑拼接成完整的絕對地址。程式碼如下:
var a = document.createElement('a');
a.id = '_defineAbsoluteUrl_';
a.style.display = 'none';
document.body.appendChild(a);

function getModuleAbsoluteUrl(path) {
    a.href = path;
    return a.href;
}

function parseAbsoluteUrl(url, parentDir) {
    var relativePrefix = '.',
        parentPrefix = '..',
        result;
    if (parentDir && url.indexOf(relativePrefix) === 0) {
        // 以'./'開頭的相對路徑
        return getModuleAbsoluteUrl(parentDir.replace(/[^\/]*$/, '') + url);
    }
    if (parentDir && url.indexOf(parentPrefix) === 0) {
        // 以'../'開頭的相對路徑
        return getModuleAbsoluteUrl(parentDir.replace(/[\/]*$/, '').replace(/[\/]$/, '').replace(/[^\/]*$/, '') + url);
    }
    return getModuleAbsoluteUrl(url);
}複製程式碼
  • 第二步,載入和執行模組檔案

對於JS檔案,利用script標籤實現。程式碼如下:

var head = document.getElementsByTagName('head')[0] || document.body;

function loadJsModule(url) {
    var script = document.createElement('script');
    script.charset = 'utf-8';
    script.type = 'text/javascript';
    script.onload = script.onreadystatechange = function() {
        if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
            /*
             * 載入邏輯, callback為define的回撥函式, args為所有依賴模組的陣列
             * callback.apply(window, args);
             */
            script.onload = script.onreadystatechange = null;
        }  
    };
}複製程式碼

對於文字檔案,直接用ajax實現。程式碼如下:

var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'),
      textContent = '';

xhr.onreadystatechange = function(){
    var DONE = 4, OK = 200;
    if(xhr.readyState === DONE){
        if(xhr.status === OK){
             textContent = xhr.responseText; // 返回的文字檔案
        } else{
            console.log("Error: "+ xhr.status); // 載入失敗
        }
    }
}

xhr.open('GET', url, true);// url為文字檔案的絕對路徑
xhr.send(null);複製程式碼

2. 模組依賴管理

一個模組的載入過程如下圖所示。

  • 狀態管理
    從上面可以看出,一個模組的載入可能存在以下幾種可能的狀態。
  1. 載入(load)狀態,包括未載入(preload)狀態、載入(loading)狀態和載入完畢(loaded)狀態。
  2. 正在載入依賴(pending)狀態。
  3. 模組回撥完成(finished)狀態。
    因此,需要為每個載入的模組加上狀態標誌(status),來識別目前模組的狀態。
  • 依賴分析
    在模組載入後,我們需要解析出每個模組的絕對路徑(path)、依賴模組(deps)和回撥函式(callback),然後也放在模組資訊中。模組物件管理邏輯的資料模型如下所示。

    {
      path: 'http://asdas/asda/a.js',
      deps: [{}, {}, {}],
      callback: function(){ },
      status: 'pending'
    }複製程式碼
  • 依賴迴圈
    模組很可能出現迴圈依賴的情況。也就是a模組和b模組相互依賴。依賴分為強依賴弱依賴強依賴是指,在模組回撥執行時就會使用到的依賴;反之,就是弱依賴。對於強依賴,會造成死鎖,這種情況是無法解決的。但弱依賴可以通過現將一個空的模組引用注入讓一個模組先執行,等依賴模組執行完後,再替換掉就可以了。強依賴弱依賴的例子如下:

//強依賴的例子
//A模組
define(['b.js'], function(B) {
  // 回撥執行時需要直接用到依賴模組
   B.demo = 1;
   // 其他邏輯
});
//B模組
define(['a.js'], function(A) {
   // 回撥執行時需要直接用到依賴模組
   A.demo = 1;
   // 其他邏輯
});複製程式碼
// 弱依賴的例子
// A模組
define(['b.js'], function(B) {
    // 回撥執行時不會直接執行依賴模組
    function test() {
        B.demo = 1;
    }
    return {testFunc: test}
});
//B模組
define(['a.js'], function(A) {
    // 回撥執行時不會直接執行依賴模組
    function test() {
        A.demo = 1;
    }
    return {testFunc: test}
});複製程式碼

3. 對外暴露define方法

對於define函式,需要遍歷所有的未處理js指令碼(包括內聯外聯),然後執行模組的載入。這裡對於內聯外聯指令碼中的define,要做分別處理。主要原因有兩點:

  1. 內斂指令碼不需要載入操作。
  2. 內斂指令碼中define的模組的回撥輸出是不能作為其他模組的依賴的。
var handledScriptList = [];
window.define = function(deps, callback) {
    var scripts = document.getElementsByTagName('script'),
        defineReg = /s*define\s*\(\[.*\]\s*\,\s*function\s*\(.*\)\s*\{/,
        script;

    for (var i = scripts.length - 1; i >= 0; i--) {
        script = list[i];
        if (handledScriptList.indexOf(script.src) < 0) {
            handledScriptList.push(script.src);
            if (script.innerHTML.search(defineReg) >= 0) {
                // 內斂指令碼直接進行模組依賴檢查。
            } else {
                // 外聯指令碼的首先要監聽指令碼載入
            }
        }
    }
};複製程式碼

上面就是對實現一個模組化工具所涉及核心問題的描述。完整的程式碼點我

相關文章