# 前端工程與模塊化框架
一直醞釀著寫一篇關于模塊化框架的文章,因為模塊化框架是前端工程中的 ``最為核心的部分`` 。本來又想長篇大論的寫一篇完整且嚴肅的paper,但看了 [@糖餅](https://github.com/aui) 在 [div.io](http://div.io/) 的一篇文章 《[再談 SeaJS 與 RequireJS 的差異](http://div.io/topic/430)》覺得可以借著這篇繼續談一下,加上最近spm3發布,在seajs的官網上又引來了一場 [口水戰](https://github.com/seajs/seajs/issues/454) ,我并不想參與到這場論戰中,各有所愛的事情不好評論什么,但我想從工程的角度來闡述一下已知的模塊化框架相關的問題,并給出一些新的思路,~~其實也不新啦,都實踐了2多年了~~。
> 前端模塊化框架肩負著 ``模塊管理``、``資源加載`` 兩項重要的功能,這兩項功能與工具、性能、業務、部署等工程環節都有著非常緊密的聯系。因此,模塊化框架的設計應該最高優先級考慮工程需要。
基于 [@糖餅](https://github.com/aui) 的文章 《[再談 SeaJS 與 RequireJS 的差異](http://div.io/topic/430)》,我這里還要補充一些模塊化框架在工程方面的缺點:
1. requirejs和seajs二者在加載上都有缺陷,就是模塊的依賴要等到模塊加載完成后,通過靜態分析(seajs)或者deps參數(requirejs)來獲取,這就為 ``合并請求`` 和 ``按需加載`` 帶來了實現上的矛盾:
* 要么放棄按需加載,把所有js合成一個文件,從而滿足請求合并(兩個框架的官方demo都有這樣的例子);
* 要么放棄請求合并,請求獨立的模塊文件,從而滿足按需加載。
2. AMD規范在執行callback的時候,要初始化所有依賴的模塊,而CMD只有執行到require的時候才初始化模塊。所以用AMD實現某種if-else邏輯分支加載不同的模塊的時候,就會比較麻煩了。考慮這種情況:
```javascript
//AMD for SPA
require(['page/index', 'page/detail'], function(index, detail){
//在執行回調之前,index和detail模塊的factory均執行過了
switch(location.hash){
case '#index':
index();
break;
case '#detail':
detail();
break;
}
});
```
在執行回調之前,已經同時執行了index和detail模塊的factory,而CMD只有執行到require才會調用對應模塊的factory。這種差別帶來的不僅僅是性能上的差異,也可能為開發增加一點小麻煩,比如不方便實現換膚功能,factory注意不要直接操作dom等。當然,我們可以多層嵌套require來解決這個問題,但又會引起模塊請求串行的問題。
-------------------------
> 結論:以純前端方式實現模塊化框架 **不能** 同時滿足 ``按需加載``,``請求合并`` 和 ``依賴管理`` 三個需求。
導致這個問題的根本原因是 ``純前端方式只能在運行時分析依賴關系``。
## 解決模塊化管理的新思路
由于根本問題出在 ``運行時分析依賴``,因此新思路的策略很簡單:不在運行時分析依賴。這就要借助 ``構建工具`` 做線下分析了,其基本原理就是:
> 利用構建工具在線下進行 ``模塊依賴分析``,然后把依賴關系數據寫入到構建結果中,并調用模塊化框架的 ``依賴關系聲明接口`` ,實現模塊管理、請求合并以及按需加載等功能。
舉個例子,假設我們有一個這樣的工程:
```
project
├ lib
│ └ xmd.js #模塊化框架
├ mods #模塊目錄
│ ├ a.js
│ ├ b.js
│ ├ c.js
│ ├ d.js
│ └ e.js
└ index.html #入口頁面
```
工程中,``index.html`` 的源碼內容為:
```html
<!doctype html>
...
<script src="lib/xmd.js"></script> <!-- 模塊化框架 -->
<script>
//等待構建工具生成數據替換 `__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`` 的源碼內容為(采用類似CMD的書寫規范):
```javascript
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');
};
});
```
## 具體實現過程
1. 用工具在下線對工程文件進行掃描,得到依賴關系表:
```json
{
"a" : [ "b", "c" ],
"b" : [ "d" ]
}
```
2. 工具把依賴表構建到頁面或者腳本中,并調用模塊化框架的配置接口,``index.html``的構建結果為:
```html
<!doctype html>
...
<script src="lib/xmd.js"></script> <!-- 模塊化框架 -->
<script>
//構建工具生成的依賴數據
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. 模塊化框架根據依賴表加載資源,比如上述例子,入口需要加載a、e兩個模塊,查表得知完整依賴關系,配合combo服務,可以發起一個合并后的請求:
http://www.example.com/??d.js,b.js,c.js,a.js,e.js
## 先來看一下這種方案的優點
1. 采用類似CMD的書寫規范(同步require函數聲明依賴),可以在執行到require語句的時候才調用模塊的factory。
1. 雖然采用CMD書寫規范,但放棄了運行時分析依賴,改成工具輸出依賴表,因此 ``依賴分析完成后可以壓縮掉require關鍵字``
1. 框架并沒有嚴格依賴工具,它只是約定了一種數據結構。不使用工具,人工維護 ``require.config({...})`` 相關的數據也是可以的。對于小項目,文件全部合并的情況,更加不需要deps表了,只要在入口的require.async調用之前加載所有模塊化的文件,依賴關系無需額外維護
1. 構建工具設計非常簡單,而且可靠。工作就是掃描模塊文件目錄,得到依賴表,JSON序列化之后插入到構建代碼中
1. 由于框架預先知道所有模塊的依賴關系,因此可以借助combo服務實現``請求合并``,而不用等到一級模塊加載完成才能知道后續的依賴關系。
1. 如果構建工具可以自動包裝define函數,那么整個系統開發起來會感覺跟nodejs非常接近,比較舒服。
## 再來討論一下這種方案的缺點:
由于采用require函數作為依賴標記,因此如果需要變量方式require,需要額外聲明,這個時候可以實現兼容AMD規范寫法,比如
```javascript
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函數中的 ``deps`` 參數,或者factory內的require都作為依賴聲明標記來識別,這樣工程性就比較完備了。
但不管怎樣, ``線下分析始終依靠了字面量信息``,所以開發上可能會有一定的局限性,但總的來說瑕不掩瑜。
> 希望本文能為前端模塊化框架的作者帶來一些新的思路。沒有必要爭論規范,工程問題才是最根本的問題。
- 前端篇
- 常用知識點
- 表單處理
- 前后端分離
- 提供模板渲染工具
- 頁面優化
- css3動畫部分
- 前端工程與模塊化框架
- 服務器XML標簽用法
- 微信JSSDK
- 小技巧
- 純CSS實現自適應正方形
- 通用媒體查詢
- css 黑科技
- H5性能優化方案
- 10個最常見的 HTML5
- 常見坑
- 資源收集
- 前端組件化開發實踐
- 應用秒開計劃
- AJAX API部分
- 靜態資源處理優化
- 后端篇
- 微信對接與管理
- 微信消息處理
- API插件開發
- Plugin開發
- 后端插件開發
- 組件開發
- XML標簽開發
- RESTFUL設計
- Admin GUI
- 設計篇
- 設計規范
- 微信開發庫v.js
- 使用方法
- 微信JSSDK集成
- 調試面板使用
- 插件-http功能
- 插件-layer彈出層
- 插件-music 音樂播放器
- 插件-store 本地存儲
- 插件 emitter 事件管理器
- 插件-shake 搖動功能
- 插件-lazyload 延遲加載
- 插件-t 模板渲染
- 插件-ani 動畫功能
- 插件-is 類型偵測器
- 插件-ease 緩動函數庫
- 插件-os 設備檢測
- 插件 $ 類Jquery插件
- 插件-md5 散列計算
- 插件-svg動畫loading
- 后臺頁面成功GUI
- 列表渲染List
- 表單生成Config
- 樹狀列表Tree
- 排序操作Sort
- Js 風格指南
- Vuep
- 內置動畫庫
- 組件庫
- 內置插件庫
- PSD自動切圖