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

目前,一個典型的前端項目技術框架的選型主要包括如下三個方面:javascript

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

本篇介紹的是JS模塊化
JS模塊化是隨着前端技術的發展,前端代碼爆炸式增加後,工程化所採起的必然措施。目前模塊化的思想分爲CommonJS、AMD和CMD。有關三者的區別,你們基本都多少有所瞭解,並且資料不少,這裏就再也不贅述。html

模塊化的核心思想:前端

  1. 拆分。將js代碼按功能邏輯拆分紅多個可複用的js代碼文件(模塊)。
  2. 加載。如何將模塊進行加載執行和輸出。
  3. 注入。可以將一個js模塊的輸出注入到另外一個js模塊中。
  4. 依賴管理。前端工程模塊數量衆多,須要來管理模塊之間的依賴關係。

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

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

define([
    '/lib/util.js', //絕對路徑
    './modal/modal.js', //相對路徑
    './modal/modal.html',//文本文件
], function(Util, Modal, tpl) {
    /* * 模塊邏輯 */
    return Module;
})複製代碼

1. 模塊如何加載和執行

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

  • 第一步,獲取模塊文件的絕對路徑
    要在瀏覽器內加載文件,首先要得到對應模塊文件的完整網絡絕對地址。因爲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標籤實現。代碼以下:ajax

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 {
                // 外聯腳本的首先要監聽腳本加載
            }
        }
    }
};複製代碼

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

相關文章
相關標籤/搜索