1. 程式人生 > >前端模組化的作用

前端模組化的作用

相信很多人都用過 seajs、 requirejs 等這些模組載入器,他們都是十分便捷的工程管理工具,簡化了程式碼的結構,更重要的是消除了各種檔案依賴和命名衝突問題,並利用 AMD / CMD 規範統一了格式。然而你瞭解模組化的作用嗎?下面主要講述模組化能解決哪些問題。

命名衝突

做專案時,常常會將一些通用的功能抽象出來,獨立成一個個函式,比如

//util.js

function formate(arr) {
  // 實現程式碼
}

function isEmpty(str) {
  // 實現程式碼
} 

把這些函式統一放在 util.js 裡。需要用到時,引入該檔案就行。這一切工作得很好,同事也很感激提供了這麼便利的工具包。

直到團隊越來越大,開始有人抱怨:我想定義一個 formate 方法格式化時間,但 util.js 裡已經定義了一個。還有的同學不知道util裡定義了formate方法,自定義了一個formate函式,然後這個頁面其他地方用到formate的地方都報錯了。。。

大團隊下使用這種方式封裝公用工具就會變成全域性變數的災難,一不小心就會有變數命名衝突的問題。 後面有人提出了採用的用自執行函式來包裝程式碼或jQuery風格的匿名自執行函式來試圖解決這個問題,程式碼如下

//util.js

//用自執行函式來包裝程式碼
var Util = function(){
     return {
          formate : function
(c){
//...... }, isEmpty: function(){ //...... } } }() //jQuery風格的匿名自執行函式 (function(window){ //程式碼 window.jQuery = window.$ = jQuery;//通過給window新增屬性而暴漏到全域性 })(window);

這樣function內部的變數就對全域性隱藏了,達到是封裝的目的。但是這樣還是有缺陷的:所需依賴還是得外部提前提供、還是增加了全域性變數。 作為前端業界的標杆,YUI 團隊下定決心解決這一問題。在 YUI3 專案中,引入了一種新的名稱空間機制

。於是 util.js 裡的程式碼變成了

//util.js

YUI().use('util', function (Y) {
  // Node 模組已載入好
  // 下面可以通過 Y 來呼叫
  var foo = Y.one('#formate');
});

//YUI 通過沙箱機制,很好的解決了名稱空間過長的問題。然而,也帶來了新問題。
YUI().use('util1', 'util2', function (Y) {
  Y.formate();  // 如果util1 和util2裡都有formate方法,
                // 那麼formate 方法究竟是模組 util1 還是 util12 提供的?
});

看似簡單的命名衝突,實際解決起來並不簡單,再來看另一個常見問題。

繁雜的檔案依賴

基於 util.js,同事寫了一個很實用的元件 component.js,使用方式很簡單,先引入 util.js,再引入 component.js,因為 component.js 依賴於 util.js。

<script src="util.js"></script>
<script src="component.js"></script>
<script>
  Component.init({ /* 傳入初始化引數 */ });
</script>

新來的同事在使用 component.js 時,不知道它依賴於 util.js ,總會來詢問為什麼 component.js 有問題。通過一番排查,發現導致錯誤的原因經常是在 component.js 前沒有引入 util.js,因此 component.js 無法正常工作。上面的檔案依賴還在可控範圍內。當專案越來越複雜,眾多檔案之間的依賴經常會讓人抓狂。 在前端頁面裡,大部分指令碼的依賴目前依舊是通過人肉的方式保證。當團隊比較小時,這不會有什麼問題。當團隊越來越大,公司業務越來越複雜後,依賴問題如果不解決,就會成為大問題。

模組化載入解決方案

隨著Node.js的到來,伺服器上通過require載入資源是直接讀取檔案的,因此中間所需的時間可以忽略不計,但是在瀏覽器這種需要依賴HTTP獲取資源的就不行了,資源的獲取所需的時間不確定,這就導致必須使用非同步機制,原理即利用script標籤的async屬性來非同步載入js檔案,代表主要有2個:

基於 AMD 的RequireJS 基於 CMD 的SeaJS

它們分別在瀏覽器實現了define、require及module的核心功能,雖然兩者的目標是一致的,但是實現的方式或者說是思路,還是有些區別的,AMD偏向於依賴前置,CMD偏向於用到時才執行的思路,從而導致了依賴項的載入和執行時間點會不同。前面例子中的 util.js 變成

// CMD
define(function (require) {
    var util1 = require('./util1'); // <- 執行到此處才開始載入並執行模組util1
    var util2 = require('./util2'); // <- 執行到此處才開始載入並執行模組util2
    util1.formate(); 
    // more code ..
})
// AMD
//util1.js
define(function(require, exports) {
  exports.formate = function (arr) {
    // 實現程式碼
  };

  exports.isEmpty = function (str) {
    // 實現程式碼
  };
});

//main.js
define(
    ['./util1', './util2'], // <- 前置宣告,也就是在主體執行前就已經載入並運行了模組a和模組b
    function (util1, util2) {
        util1.formate();
        // more code ..
    }
)

我們通過 require(‘./util1.js’) 就可以拿到 util1.js 中通過 exports 暴露的介面,使用util1裡的formate方法即util1.formate();,並不會有 util1.js 和 util2.js 方法名衝突問題。這裡的 require 可以認為是給 JavaScript 語言增加的一個 語法關鍵字,通過 require 可以獲取其他模組提供的介面。

好好琢磨以上程式碼,我相信你已經看到了 RequireJS 和 SeaJS 帶來的兩大好處:

1、通過 exports 暴露介面。這意味著不需要名稱空間了,更不需要全域性變數。這是一種徹底的命名衝突解決方案。 2、通過 require 引入依賴。這可以讓依賴內建,開發者只需關心當前模組的依賴,其他事情 RequireJS 和 SeaJS 都會自動處理好。對模組開發者來說,這是一種很好的 關注度分離,能讓程式設計師更多地享受編碼的樂趣。

小結

除了解決命名衝突和依賴管理,使用模組化開發還可以帶來很多好處:

  1. 模組的版本管理。通過別名等配置,配合構建工具,可以比較輕鬆地實現模組的版本管理。
  2. 提高可維護性。模組化可以讓每個檔案的職責單一,非常有利於程式碼的維護。Sea.js 還提供了 nocache、debug等外掛,擁有線上除錯等功能,能比較明顯地提升效率。
  3. 前端效能優化。RequireJS 和 SeaJS 都是通過非同步載入模組,這對頁面效能非常有益。Sea.js 還提供了 combo、flush等外掛,配合服務端,可以很好地對頁面效能進行調優。
  4. 跨環境共享模組。CMD 模組定義規範與 Node.js 的模組規範非常相近。通過 Sea.js 的 Node.js版本,可以很方便實現模組的跨伺服器和瀏覽器共享。