前端工程與模塊化框架

2018-02-24 15:49 更新

原文出處:https://github.com/fouber/blog/issues/4
作者:fouber

目錄

本文最先發(fā)表在?DIV.IO?- 高質(zhì)量前端社區(qū),歡迎大家圍觀

不要再求驗(yàn)證碼了,這個(gè)blog目前有800+人訂閱,求驗(yàn)證沒什么的很影響其他訂閱者,可以在div.io上申請(qǐng),定期會(huì)有同學(xué)發(fā)放的。。。


一直醞釀著寫一篇關(guān)于模塊化框架的文章,因?yàn)槟K化框架是前端工程中的?最為核心的部分?。本來又想長(zhǎng)篇大論的寫一篇完整且嚴(yán)肅的paper,但看了?@糖餅?在?DIV.IO?的一篇文章 《再談 SeaJS 與 RequireJS 的差異》覺得可以借著這篇繼續(xù)談一下,加上最近spm3發(fā)布,在seajs的官網(wǎng)上又引來了一場(chǎng)?口水戰(zhàn)?,我并不想?yún)⑴c到這場(chǎng)論戰(zhàn)中,各有所愛的事情不好評(píng)論什么,但我想從工程的角度來闡述一下已知的模塊化框架相關(guān)的問題,并給出一些新的思路,其實(shí)也不新啦,都實(shí)踐了2多年了

前端模塊化框架肩負(fù)著?模塊管理、資源加載?兩項(xiàng)重要的功能,這兩項(xiàng)功能與工具、性能、業(yè)務(wù)、部署等工程環(huán)節(jié)都有著非常緊密的聯(lián)系。因此,模塊化框架的設(shè)計(jì)應(yīng)該最高優(yōu)先級(jí)考慮工程需要。

基于?@糖餅?的文章 《再談 SeaJS 與 RequireJS 的差異》,我這里還要補(bǔ)充一些模塊化框架在工程方面的缺點(diǎn):

  1. requirejs和seajs二者在加載上都有缺陷,就是模塊的依賴要等到模塊加載完成后,通過靜態(tài)分析(seajs)或者deps參數(shù)(requirejs)來獲取,這就為?合并請(qǐng)求?和?按需加載?帶來了實(shí)現(xiàn)上的矛盾:
    • 要么放棄按需加載,把所有js合成一個(gè)文件,從而滿足請(qǐng)求合并(兩個(gè)框架的官方demo都有這樣的例子);
    • 要么放棄請(qǐng)求合并,請(qǐng)求獨(dú)立的模塊文件,從而滿足按需加載。
  2. AMD規(guī)范在執(zhí)行callback的時(shí)候,要初始化所有依賴的模塊,而CMD只有執(zhí)行到require的時(shí)候才初始化模塊。所以用AMD實(shí)現(xiàn)某種if-else邏輯分支加載不同的模塊的時(shí)候,就會(huì)比較麻煩了。考慮這種情況:

    //AMD for SPA
    require(['page/index', 'page/detail'], function(index, detail){
        //在執(zhí)行回調(diào)之前,index和detail模塊的factory均執(zhí)行過了
        switch(location.hash){
            case '#index':
                index();
            break;
            case '#detail':
                detail();
            break;
        }
    });

    在執(zhí)行回調(diào)之前,已經(jīng)同時(shí)執(zhí)行了index和detail模塊的factory,而CMD只有執(zhí)行到require才會(huì)調(diào)用對(duì)應(yīng)模塊的factory。這種差別帶來的不僅僅是性能上的差異,也可能為開發(fā)增加一點(diǎn)小麻煩,比如不方便實(shí)現(xiàn)換膚功能,factory注意不要直接操作dom等。當(dāng)然,我們可以多層嵌套require來解決這個(gè)問題,但又會(huì)引起模塊請(qǐng)求串行的問題。


結(jié)論:以純前端方式實(shí)現(xiàn)模塊化框架?不能?同時(shí)滿足?按需加載,請(qǐng)求合并?和?依賴管理?三個(gè)需求。

導(dǎo)致這個(gè)問題的根本原因是?純前端方式只能在運(yùn)行時(shí)分析依賴關(guān)系。

解決模塊化管理的新思路

由于根本問題出在?運(yùn)行時(shí)分析依賴,因此新思路的策略很簡(jiǎn)單:不在運(yùn)行時(shí)分析依賴。這就要借助?構(gòu)建工具做線下分析了,其基本原理就是:

利用構(gòu)建工具在線下進(jìn)行?模塊依賴分析,然后把依賴關(guān)系數(shù)據(jù)寫入到構(gòu)建結(jié)果中,并調(diào)用模塊化框架的?依賴關(guān)系聲明接口?,實(shí)現(xiàn)模塊管理、請(qǐng)求合并以及按需加載等功能。

舉個(gè)例子,假設(shè)我們有一個(gè)這樣的工程:

project
  ├ lib
  │  └ xmd.js    #模塊化框架
  ├ mods         #模塊目錄
  │  ├ a.js
  │  ├ b.js
  │  ├ c.js
  │  ├ d.js
  │  └ e.js
  └ index.html   #入口頁(yè)面

工程中,index.html?的源碼內(nèi)容為:

<!doctype html>
...
<script src="https://atts.w3cschool.cn/attachments/image/cimg/xmd.js"></script>   <!-- 模塊化框架 -->
<script>
    //等待構(gòu)建工具生成數(shù)據(jù)替換 `__FRAMEWORK_CONFIG__' 變量
    require.config(__FRAMEWORK_CONFIG__);
</script>
<script>
    //用戶代碼,異步加載模塊
    require.async(['a', 'e'], function(a, e){
        //do something with a and e.
    });
</script>
...

工程中,mods/a.js?的源碼內(nèi)容為(采用類似CMD的書寫規(guī)范):

define('a', function(require, exports, module){
    console.log('a.init');
    var b = require('b');
    var c = require('c');
    exports.run = function(){
        //do something with b and c.
        console.log('a.run');
    };
});

具體實(shí)現(xiàn)過程

  1. 用工具在下線對(duì)工程文件進(jìn)行掃描,得到依賴關(guān)系表:

    {
        "a" : [ "b", "c" ],
        "b" : [ "d" ]
    }
  2. 工具把依賴表構(gòu)建到頁(yè)面或者腳本中,并調(diào)用模塊化框架的配置接口,index.html的構(gòu)建結(jié)果為:

    <!doctype html>
    ...
    <script src="https://atts.w3cschool.cn/attachments/image/cimg/xmd.js"></script>   <!-- 模塊化框架 -->
    <script>
        //構(gòu)建工具生成的依賴數(shù)據(jù)
        require.config({
            "deps" : {
                "a" : [ "b", "c" ],
                "b" : [ "d" ]
            }
        });
    </script>
    <script>
        //用戶代碼,異步加載模塊
        require.async(['a', 'e'], function(a, e){
            //do something with a and e.
        });
    </script>
  3. 模塊化框架根據(jù)依賴表加載資源,比如上述例子,入口需要加載a、e兩個(gè)模塊,查表得知完整依賴關(guān)系,配合combo服務(wù),可以發(fā)起一個(gè)合并后的請(qǐng)求:

    http://www.example.com/??d.js,b.js,c.js,a.js,e.js

先來看一下這種方案的優(yōu)點(diǎn)

  1. 采用類似CMD的書寫規(guī)范(同步require函數(shù)聲明依賴),可以在執(zhí)行到require語句的時(shí)候才調(diào)用模塊的factory。
  2. 雖然采用CMD書寫規(guī)范,但放棄了運(yùn)行時(shí)分析依賴,改成工具輸出依賴表,因此?依賴分析完成后可以壓縮掉require關(guān)鍵字
  3. 框架并沒有嚴(yán)格依賴工具,它只是約定了一種數(shù)據(jù)結(jié)構(gòu)。不使用工具,人工維護(hù)require.config({...})?相關(guān)的數(shù)據(jù)也是可以的。對(duì)于小項(xiàng)目,文件全部合并的情況,更加不需要deps表了,只要在入口的require.async調(diào)用之前加載所有模塊化的文件,依賴關(guān)系無需額外維護(hù)
  4. 構(gòu)建工具設(shè)計(jì)非常簡(jiǎn)單,而且可靠。工作就是掃描模塊文件目錄,得到依賴表,JSON序列化之后插入到構(gòu)建代碼中
  5. 由于框架預(yù)先知道所有模塊的依賴關(guān)系,因此可以借助combo服務(wù)實(shí)現(xiàn)請(qǐng)求合并,而不用等到一級(jí)模塊加載完成才能知道后續(xù)的依賴關(guān)系。
  6. 如果構(gòu)建工具可以自動(dòng)包裝define函數(shù),那么整個(gè)系統(tǒng)開發(fā)起來會(huì)感覺跟nodejs非常接近,比較舒服。

再來討論一下這種方案的缺點(diǎn)

由于采用require函數(shù)作為依賴標(biāo)記,因此如果需要變量方式require,需要額外聲明,這個(gè)時(shí)候可以實(shí)現(xiàn)兼容AMD規(guī)范寫法,比如

define('a', ['b', 'c'], function(require, exports, module){
    console.log('a.init');
    var name = isIE ? 'b' : 'c';
    var mod = require(name);
    exports.run = function(){
        //do something with mod.
        console.log('a.run');
    };
})

只要工具把define函數(shù)中的?deps?參數(shù),或者factory內(nèi)的require都作為依賴聲明標(biāo)記來識(shí)別,這樣工程性就比較完備了。

但不管怎樣,?線下分析始終依靠了字面量信息,所以開發(fā)上可能會(huì)有一定的局限性,但總的來說瑕不掩瑜。

希望本文能為前端模塊化框架的作者帶來一些新的思路。沒有必要爭(zhēng)論規(guī)范,工程問題才是最根本的問題。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)