1. 程式人生 > >前端模組化的簡單綜述(一)

前端模組化的簡單綜述(一)

剛學前端的時候,曾有一段時間很迷糊,不知道為啥突然從html檔案、js檔案和css檔案三件套,變成需要打包在伺服器才能用了。這種不明白感,隨著使用vue,weex等框架逐步熟練之後,降低不少。但依然不知道,這一路究竟發生了什麼。

  故在此,梳理整個前端模組的發展過程,為自己的疑惑提供一個完滿的解答。(有不少內容來自對網路上一些大神的部落格整理,本文會在文章最後做出引用和感謝)

目錄

0,模組的定義

  模組和模組化:

    模組:

在軟體系統設計中,模組是一個擁有明確定義的輸入輸出和特性的程式實體。如果模組的所有輸入都是實現功能必不可少的,所有輸出都有動作產生,那麼該模組即成為具有明確意義的模組。也就是說,如果少了一個輸入,模組就不能實現全部功能,它沒有不必要的輸入,每個輸入都用於產生輸出,每個輸出都是模組執行某一功能的結果,沒有未經模組的轉換就變成輸出的輸入。

總的來說,一般模組具有以下幾種特徵:

  1. 介面,模組的輸入輸出
  2. 功能,指模組實現的功能,有什麼作用
  3. 邏輯,描述模組內部如何實現需求及所需資料
  4. 狀態,指該模組的執行環境,模組間呼叫與被呼叫關係。

模組化:

    模組化就是將程式劃分成若干個獨立的模組,每個模組完成一個特定子功能,每個模組即相對獨立,又相互聯絡,他們共同完成系統指定的各項功能。 模組化的目的是為了降低軟體的複雜性。對軟體進行適當的分解,不但可以降低複雜性,而且可以減少開發工作量,從而降低軟體開發成本。

1, 前端最開始的處理:

        最自然的寫法:

function foo(){
    //...
  }
  function bar(){
      //...
  }

而只將相關的程式碼放一起這種寫法缺點很明顯:

  1:大量的函式名會"汙染"全域性變數,無法保證不與其他模組發生變數名衝突,

  2:模組成員之間的關係不能清晰看出。

  3:沒有私有變數和方法等。

針對問題1:和2:,可以用物件來緩解。

 var moduleA = {
    foo : function () {},
    bar: function () {}
  }

但是,這樣的寫法會暴露所有模組成員,內部狀態可以被外部改寫

針對這個問題,可以採用立即執行函式來處理:

 var MODULE = (function () {
    var my = {}, privateVariable = 1;
    function privateMethod() {
      // method that use in module
    }
  
    my.moduleProperty = 1;
    my.moduleMethod = function () {
      // module API.
    };
  
    return my;
  }());

立即執行函式內部宣告的變數和函式,為模組內部私有,return物件為供外部呼叫的介面。

如此方式的“模組”還可以擴充套件、繼承、新增子模組等等。

如此便形成了前端模組的雛形。

2, 前端為什麼需要模組化。

當下前端的快速發展,對網頁的構建提出了更高要求:

  • Web sites are turning into Web Apps

  • Code complexity grows as the site gets bigger

  • Highly decoupled JS files/modules is wanted

  • Deployment wants optimized code in few HTTP calls

前端通過<script>標籤引入js 來處理複雜邏輯和遠端互動。

當邏輯複雜時,開啟方式如下:

body
    script(src="zepto.js")
    script(src="jhash.js")
    script(src="fastClick.js")
    script(src="iScroll.js")
    script(src="underscore.js")
    script(src="handlebar.js")
    script(src="datacenter.js")
    script(src="deferred.js")
    script(src="util/wxbridge.js")
    script(src="util/login.js")
    script(src="util/base.js")
    script(src="util/city.js")
    script(src="util/date.js")
    script(src="util/cookie.js")
    script(src="app.js")

        各個檔案的引入順序非常重要,各個檔案並行載入、DOM 順序即執行順序

而用script標籤來引入js函式檔案存在問題:

  • 維護困難
  • 依賴模糊
  • 請求過多

只用引入<script>標籤帶來的問題,越來越難以滿足前端發展的需求。因此模組化成了一個合理的選項

3,common JS規範

2009年,美國程式設計師Ryan Dahl創造了node.js專案,將javascript語言用於伺服器端程式設計。這標誌"Javascript模組化程式設計"正式誕生。node.js的模組系統,就是參照CommonJS規範實現的。

在有了伺服器端模組以後,很自然地,大家就想要客戶端模組。而且最好兩者相容,一個模組不用修改,在伺服器和瀏覽器都可以執行。但是,由於一個重大的侷限,使得CommonJS規範不適用於瀏覽器環境。

var math = require('math');

math.add(2, 3);

如上面的程式碼,math.add實在require之後執行,因此必須等math.js載入完成。

這對伺服器端不是一個問題,因為所有的模組都存放在本地硬碟,可以同步載入完成,等待時間就是硬碟的讀取時間。但是,對於瀏覽器,因為模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間而使瀏覽器處於"假死"狀態。因此,瀏覽器端的模組,不能採用"同步載入"(synchronous),只能採用"非同步載入"(asynchronous)。這就是AMD規範誕生的背景。

4,AMD規範

RequireJS是一個工具庫,主要用於客戶端的模組管理。它可以讓客戶端的程式碼分成一個個模組,實現非同步或動態載入,從而提高程式碼的效能和可維護性。它的模組管理遵守AMD規範(Asynchronous Module Definition)。

RequireJS的基本思想是,通過define方法,將程式碼定義為模組;通過require方法,實現程式碼的模組載入。

4.1 嵌入網頁

<script data-main="scripts/main" src="scripts/require.js"></script>

程式碼的data-main屬性不可省略,用於指定主程式碼所在的指令碼檔案

4.2 define 方法:定義模組

按照是否依賴其他模組,可以分成獨立模組和非獨立模組。

    獨立模組的寫法:

define({
    method1: function() {},
    method2: function() {},
});


// 當然也可以寫為如下方式
define(function () {
  return {
    method1: function() {},
	method2: function() {},
 };
});

return 返回的是模組的介面。

    非獨立模組

define(['module1', 'module2'], function(m1, m2) {
   ...
});



4.3 require方法:呼叫模組

require(['foo', 'bar'], function ( foo, bar ) {
        foo.doSomething();
});
// 當有過多依賴時,可以用commonJS 寫法:
define(
    function (require) {
        var dep1 = require('dep1'),
            dep2 = require('dep2'),
            dep3 = require('dep3'),
            dep4 = require('dep4'),

            ...
    }

});

上面方法表示載入foo和bar兩個模組,當這兩個模組都載入成功後,執行一個回撥函式。該回調函式就用來完成具體的任務。

require方法也可以用在define方法內部。比如上面的commonJS風格的寫法使用。

5, CMD規範

CMD 即Common Module Definition通用模組定義,CMD規範是國內發展出來的,CMD有個瀏覽器的實現是玉伯的SeaJSSeaJS要解決的問題和requireJS一樣,只不過在模組定義方式和模組載入(可以說執行、解析)時機上有所不同。

以下是對seajs使用的簡單介紹:

 5.1 seajs頁面的引入

<body>
<h1 id="title">seajs demo</h1>
<script src="sea-module/sea.js"></script>
<script>
    seajs.use('./static/main.js');
</script>
</body>

首先是引入sea.js檔案,然後是通過seajs.use載入main.js檔案。main.js是一個“入口”檔案。

5.2 模組的寫法

比如定義一個changeText.js模組

define(function (require, exports, module) {
    var textContent = 'message from module changeText';
    module.exports = {
      text: textContent
    }
})

5.3 引入模組

define(function (require, exports, module) {
    var changeText = require('../static/changeText.js');
    var title = document.getElementById('title');
    title.innerHTML = changeText.text;
})

此時,大功告成,會在html頁面上顯示 “message from module changeText" 

5.4 別名 (seajs.config, alias)

 seajs.config({
   alias:{
     'changeText':'../static/changeText.js'
   }
 });

通過config中alias給'../static/changeText.js'設定了別名changeText,現在main中引用changeText模組就可以直接寫成這樣了var changeText = require('changeText')

5.5 seajs.use回撥函式

seajs.use(['main','jquery'],function(main,$) {
    $('#title').after('<button id="show">showText</button>');
    $('#show').on('click',function() {
         main.showText()
    })
});

上述程式碼我們載入了兩個模組,並把它們輸出的物件傳參給main和$變數,通過點選事件呼叫main中的showText方法,而showText方法呼叫了changeText中的init方法。

參考連結: