回憶一下我們在工程開發中對目錄結構的定義,一般分為兩種,單頁面多模組,多頁面多模組。在單頁面多模組的工程結構裡,我們會考慮模組的複用性,比如:如何將公共的東西(樣式、函式等)提取出來方便其他模組複用。在多頁面多模組的場景中,也是一樣,不過除了把全域性共用的樣式和方法提取到公共目錄外,我們還會將多個地方都會用到的模組作為通用模組處理。
一、通常開發模式的問題探討
下圖是一個單頁面多模組的工程目錄結構圖:
.
├── 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... });
三、小結
還有很多東西不在主題的討論範圍內,就不一一列舉出來了。
專案開發參與的人越多,程式碼就越難維護,約束只是一時的,程式設計方式、編碼格式等的約束並不能從根本上解決問題,一旦約束的點未覆蓋,結構就會開始散亂,最後必然又會迎來一次整體的重構。
方法和結果不能改變習慣,所以我們應該從模式出發。