前端工程與模塊化框架

一直醞釀着寫一篇關於模塊化框架的文章,由於模塊化框架是前端工程中的 最爲核心的部分 。原本又想長篇大論的寫一篇完整且嚴肅的paper,但看了 @糖餅 在 DIV.IO 的一篇文章 《再談 SeaJS 與 RequireJS 的差別》以爲能夠藉着這篇繼續談一下,加上最近spm3發佈,在seajs的官網上又引來了一場 口水戰 ,我並不想參與到這場論戰中,各有所愛的事情很差評論什麼,但我想從工程的角度來闡述一下已知的模塊化框架相關的問題,並給出一些新的思路,其實也不新啦,都實踐了2多年了javascript

前端模塊化框架肩負着 模塊管理資源加載 兩項重要的功能,這兩項功能與工具、性能、業務、部署等工程環節都有着很是緊密的聯繫。所以,模塊化框架的設計應該最高優先級考慮工程須要。html

基於 @糖餅 的文章 《再談 SeaJS 與 RequireJS 的差別》,我這裏還要補充一些模塊化框架在工程方面的缺點:前端

  • requirejs和seajs兩者在加載上都有缺陷,就是模塊的依賴要等到模塊加載完成後,經過靜態分析(seajs)或者deps參數(requirejs)來獲取,這就爲 合併請求 和 按需加載 帶來了實現上的矛盾:
    • 要麼放棄按需加載,把全部js合成一個文件,從而知足請求合併(兩個框架的官方demo都有這樣的例子);
    • 要麼放棄請求合併,請求獨立的模塊文件,從而知足按需加載。
  • AMD規範在執行callback的時候,要初始化全部依賴的模塊,而CMD只有執行到require的時候才初始化模塊。因此用AMD實現某種if-else邏輯分支加載不一樣的模塊的時候,就會比較麻煩了。考慮這種狀況:
//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來解決這個問題,但又會引發模塊請求串行的問題。java


結論:以純前端方式實現模塊化框架 不能 同時知足 按需加載請求合併 和 依賴管理 三個需求。node

致使這個問題的根本緣由是 純前端方式只能在運行時分析依賴關係git

解決模塊化管理的新思路

因爲根本問題出在 運行時分析依賴,所以新思路的策略很簡單:不在運行時分析依賴。這就要藉助 構建工具 作線下分析了,其基本原理就是:github

利用構建工具在線下進行 模塊依賴分析,而後把依賴關係數據寫入到構建結果中,並調用模塊化框架的 依賴關係聲明接口 ,實現模塊管理、請求合併以及按需加載等功能。json

舉個例子,假設咱們有一個這樣的工程:數據結構

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

工程中,index.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的書寫規範):

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'); }; });

具體實現過程

  • 用工具在下線對工程文件進行掃描,獲得依賴關係表:
{ "a" : [ "b", "c" ], "b" : [ "d" ] }
  • 工具把依賴表構建到頁面或者腳本中,並調用模塊化框架的配置接口,index.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>
  • 模塊化框架根據依賴表加載資源,好比上述例子,入口須要加載a、e兩個模塊,查表得知完整依賴關係,配合combo服務,能夠發起一個合併後的請求:

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

先來看一下這種方案的優勢

  • 採用相似CMD的書寫規範(同步require函數聲明依賴),能夠在執行到require語句的時候才調用模塊的factory。
  • 雖然採用CMD書寫規範,但放棄了運行時分析依賴,改爲工具輸出依賴表,所以 依賴分析完成後能夠壓縮掉require關鍵字
  • 框架並無嚴格依賴工具,它只是約定了一種數據結構。不使用工具,人工維護 require.config({...}) 相關的數據也是能夠的。對於小項目,文件所有合併的狀況,更加不須要deps表了,只要在入口的require.async調用以前加載全部模塊化的文件,依賴關係無需額外維護
  • 構建工具設計很是簡單,並且可靠。工做就是掃描模塊文件目錄,獲得依賴表,JSON序列化以後插入到構建代碼中
  • 因爲框架預先知道全部模塊的依賴關係,所以能夠藉助combo服務實現請求合併,而不用等到一級模塊加載完成才能知道後續的依賴關係。
  • 若是構建工具能夠自動包裝define函數,那麼整個系統開發起來會感受跟nodejs很是接近,比較舒服。

再來討論一下這種方案的缺點:

因爲採用require函數做爲依賴標記,所以若是須要變量方式require,須要額外聲明,這個時候能夠實現兼容AMD規範寫法,好比

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都做爲依賴聲明標記來識別,這樣工程性就比較完備了。

但無論怎樣, 線下分析始終依靠了字面量信息,因此開發上可能會有必定的侷限性,但總的來講瑕不掩瑜。

相關文章
相關標籤/搜索