前端工程架構探討


回憶一下我們在工程開發中對目錄結構的定義,一般分為兩種,單頁面多模塊,多頁面多模塊。在單頁面多模塊的工程結構里,我們會考慮模塊的復用性,比如:如何將公共的東西(樣式、函數等)提取出來方便其他模塊復用。在多頁面多模塊的場景中,也是一樣,不過除了把全局共用的樣式和方法提取到公共目錄外,我們還會將多個地方都會用到的模塊作為通用模塊處理。

一、通常開發模式的問題探討

下圖是一個單頁面多模塊的工程目錄結構圖:

.
├── Gruntfile.js
├── package.json
├── build
└── src
    ├── base
    │   ├── base.sass
    │   └── global.js
    ├── mods
    │   ├── preference
    │   │   ├── index.js
    │   │   ├── index.sass
    │   │   └── index.xtpl.html
    │   ├── promo
    │   ├── qr
    │   └── response
    └── index.js

我們把源碼放在 src 文件夾里面,公共的文件(iconfont 、sprite 圖片、CSS 和 JS 等)放到 base 目錄下,頁面中的每個模塊都會在 mods 下新建一個文件夾,使用 index.js 來管理模塊的渲染。

// index.js
define(function(require){
    var Lazyload = require('lazyload');
    var Preference = require('./mods/preference/index');
    var Qr = require('./mods/qr/index');
    var Promo = require('./mods/promo/index');
    var Response = require('./mods/response/index');

    new Response();
    if(xxx){
        new Promo();
    }
    Lazyload(function(){
        new Qr();
        new Preference();
    });
});

這樣的工程結構是十分通用,結構也比較清晰的,不過在模塊的管理上,這里會存在兩個問題:

  • AB模塊存在較多的共用代碼,我們有兩種方式處理,一是將公共部分提取出來放到 base 目錄下,二是 B 模塊直接根據相對路徑引用 A 模塊。一旦業務上有需求,說 A 模塊要下線,那下線之后,第一種方案放置在 base 目錄下的代碼就不合理了,第二種方案中 B 模塊就不能用了,需要將 A 模塊的東西部分遷移到 B 模塊。
  • 問題 1 的逆過程:線上目前存在 A 模塊,業務上需求需要添加跟 A 模塊相似的 B 模塊,如果想直接復用 A 模塊的代碼,一種方式是更小顆粒地分拆 A 模塊,然后 B 使用相對路徑引用 A,另一種方式是將 A 的共用代碼提取出來放到 base 下。兩種處理方式都有一定的工作量,而且還會出現問題 1 提到的問題。

其實說到底還是模塊的耦合度過高,只要模塊之間存在交集,一個模塊的改動就可能會影響到其他模塊。多人開發中,這里還存在其他方面的問題:

  • 並不是每個開發者對接手的項目都有一個全局的把控,下線一個模塊時,會不太敢刪除 base 目錄下跟該模塊相關的東西,甚至都不太敢刪除這個模塊,只是在 index.js 中注釋了這個模塊的初始化。日積月累,冗余代碼便會滲入到項目的各個地方…
  • 修改一個模塊需要編譯打包所有的代碼(部分情況下需要編譯,比如存在離線模板,將 html 模塊編譯成 js),這樣的調試效率十分低下,而且這個模塊出錯,就可能造成整個程序的崩潰。
  • 代碼歷史版本管理的顆粒度不夠,比如我修改了 A、B、C 三個模塊,依次上線了三次,現在要回滾修改 A 的操作,如何處理?如果 ABC 三個模塊都能夠利用代碼管理工具管理代碼,那回滾就方便多了。

二、模塊化處理

去耦合的方式就是讓模塊之間共用的東西減少,當模塊之間不存在共用內容時,耦合度基本就是零了。

.
├── init.js
├── build
└── src
    ├── preference <git>
    │   ├── index.js
    │   ├── index.sass
    │   └── index.xtpl.html
    ├── promo <git>
    ├── qr <git>
    └── response <git>

如上圖所示,與之前的結構相比,已經少了很多東西:

  • index.js 初始化模塊的東西不見了,多了一個 init.js
  • base 目錄不見了
  • 每個模塊都變成了一個 git 倉庫

1. 腳本的初始化

先看看 init.js 在干啥:

// init.js
var $mods = $("[tb-mods]");
$mods.each(functon($mod){
    if($mod.attr("finish") !== FINISH_TAG) {
        $mod.attr("finish", FINISH_TAG);
        // 需要懶加載便懶加載
        if($mod.attr("lazyload")){
            Lazyload($mod);
            return;
        } 
        // 否則直接初始化
        S.use($mod.attr("path"), function(S, Mod){
            new Mod($mod);
        });
    }
});

function Lazyload(){
    // code here..
}

init.js 不再對模塊進行精確初始化,文檔從上往下遍歷,找到模塊便直接初始化,如果需要懶加載就加入到懶加載隊列,開發者不用理會頁面上有多少模塊,更不用理會各個模塊叫做什么名字。

index.js 中 require 很多很多模塊,每次添加一個模塊或者刪除模塊都要改動這個文件,而是用 init.js 不會存在這個問題。

2. 模塊的版本控制

<!-- index.xtpl.html -->
<div tb-mods lazyload path="tb/promo/1.0.0"></div>
<div tb-mods lazyload path="tb/qr/2.0.0"></div>
<div tb-mods lazyload  path="tb/preference/2.2.1"></div>
<div tb-mods path="tb/response/3.0.2"></div>

頁面上的 DOM 就是標識,存在 DOM 屬性標識就執行這個標識對應的腳本,執行順序就是 DOM 的擺放順序。

每個模塊代碼都使用單個 git 倉庫管理,這樣能夠更好地追蹤單個模塊的修改記錄和版本,也可以解決上面提出的問題(依次修改 ABC 模塊,並上線了三次,如果需要回滾 A 模塊,則 BC 模塊的修改也要跟着滾回去)。

3. ABTest 需求

修改一個模塊后,只需要修改他在 DOM 的版本號即可上線。如果遇到 ABTest 的需求,那也十分好辦了:

<!-- index.xtpl.html -->
{{#if condition}}
<div tb-mods lazyload path="tb/promo/1.0.0"></div>
{{else}}
<div tb-mods path="tb/promo/2.0.0"></div>
{{/if}}
<div tb-mods lazyload path="tb/qr/2.0.0"></div>
<div tb-mods path="tb/response/3.0.2"></div>

tb/promo 目前有兩個版本,1.0.0 和 2.0.0,需求是兩個版本以 50% 的概率出現,直接在 index.xtpl.html 做如上修改,程序是十分清晰的。

4. 公共文件的處理

那么,公共的代碼跑哪里去了?其實我們並不希望有公共的代碼產生,上一節中已經提出了耦合給我們帶來的維護問題,但是一個項目中必然會有大量可復用的東西,尤其是當頁面出現很多相似模塊的時候。

1)模塊的復用

一個模塊的渲染,需要兩樣東西,渲染殼子(模板) + 數據,渲染的殼子可能是一樣的,只是數據源不一樣,很多情況下我們可以復用一套 CSS 和 JS 代碼,通過下面的方式:

<!-- index.xtpl.html -->
<div tb-mods lazyload path="tb/promo/1.0.0" source="data/st/json/v2"></div>
<div tb-mods lazyload path="tb/promo/1.0.0" source="data/wt/json/v1"></div>

在兩個相似模塊中,我們使用的是同一套 js - tb/promo/1.0.0,但是使用了兩個不同的數據源 data/st/json/v2, data/wt/json/v1

// init.js
$mods.each(functon($mod){
    if($mod.attr("finish") !== FINISH_TAG) {
        //...
        S.use($mod.attr("path"), function(S, Mod){
            // 將數據源傳入
            new Mod($mod, $mod.attr("source"));
        });
        //...
    }
});

在初始化腳本中,我們將模塊需要用到的數據源傳入到模塊初始化程序中,這樣頁面就成功的復用了 tb/promo/1.0.0 的資源。

2)CSS 的復用問題使用 less 的 mixin 處理

@a: red;
@b: white;
.s1(){
    color: @a;
    background: @b;
}
.s2 {
    color: @a;
    background: @b;
}

LESS 是 CSS 的預處理語言,上面的代碼打包之后,.s1 是不存在的,只有 .s2 會被打包出來,但是兩者都可以 mixin 到其他類中:

.s {
    .s1;
    .s2;
}

利用這個特點,我們可以把共用的 css 都包裝成類似 .s1 的 less 代碼,模塊中需要的時候就 mixin,不需要的話,放在那里也沒關系,不會造成代碼冗余。

3)JavaScript 的代碼復用問題

頁面級別的 JS 代碼其實並不多,比如我們平時用的比較頻繁的有 Slide、Lazyload、Tab、Storage 等,但這些東西都是以組件的形式引入到頁面中。仔細想一想,JS 中哪些代碼是需要頁面共用的?相對整個項目的文件大小,共用的部分又有多少?

我們使用的基礎庫方法並不全面,比如:沒有對 URL 解析的 unparam 方法,而這個方法用的也比較多,希望放到公共部分中去。回頭想想,這樣的小函數實現起來有啥難度么,三四行代碼就能寫出來的東西,建議放到組件內部搞定。這會造成一定的代碼冗余,但是帶來的解耦收益與費力寫幾行代碼的成本相比,這完全是可以接受的。

頁面共用的統計代碼、錯誤收集代碼、數據緩存方案、組件通訊代碼等,這些量比較大、使用頗為頻繁的內容,可以封裝成組件,以組件形式引入進來。

這里還需要很多思考…

5. 模塊之間的通訊

模塊之間的通訊最讓人糾結的是,A 模塊想跟 B 模塊說話,但是 B 模塊還沒有初始化出來。所以我們需要引入一個中間人 S,每個模塊初始化成功之后都去問一問 S,有沒有人給我留言。

// B 給 A 留言,如果 A 存在,則直接將 msg 發給 A
// 如果不存在則送入 S 的消息隊列
S.tell("A", {
    from : "B",
    msg: {}
});

// A 模塊初始化的時候,獲取其他模塊的留言
S.getMessage("A", function(msg){
    // dosomething...
});

三、小結

還有很多東西不在主題的討論范圍內,就不一一列舉出來了。

項目開發參與的人越多,代碼就越難維護,約束只是一時的,編程方式、編碼格式等的約束並不能從根本上解決問題,一旦約束的點未覆蓋,結構就會開始散亂,最后必然又會迎來一次整體的重構。

方法和結果不能改變習慣,所以我們應該從模式出發。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM