淺談 JavaScript 模塊化編程

本博客同步自個人GitHub博客javascript

JavaScript自己不是一種模塊化語言,設計者在創造JavaScript之初應該也沒有想到這麼一個腳本語言的做用領域會愈來愈大。之前一個頁面的JS代碼再多也不會多到哪兒去,而如今隨着愈來愈多的JavaScript庫和框架的出現,Single-page App的流行以及Node.js的迅猛發展,若是咱們還不對本身的JS代碼進行一些模塊化的組織的話,開發過程會愈來愈困難,運行性能也會愈來愈低。所以,瞭解JS模塊化編程是很是重要的。php

簡單的模塊

什麼是模塊?我認爲將不一樣功能的函數放在一塊兒,組成一個能實現某種或某些特定功能的總體就是一個模塊,所以這樣:html

function add(a, b) {
  return a + b;
}

function divide(a, b) {
  return a / b;
}

如此簡單的兩個函數就能夠組成一個模塊,這個模塊能夠進行一些數學運算。前端

固然沒有人會這麼寫模塊。僅僅是從「型」上來看,兩個函數分散在全局環境中,這也看不出模塊的特色。模塊存在於全局變量中,應該提供一個命名空間,成爲模塊內容的入口。那麼咱們能夠將函數包裹在一個對象中:java

var math = {
  add: function(a, b) {
    return a + b;
  },
  divide: function(a, b) {
    return a / b;
  }
}

這樣看起來彷佛有模塊的「型」了。可是這樣還不完善,math 中的全部成員都是對外暴露的,若是其中有一些變量不但願被修改的話那就有風險了。爲了防止世界被破壞,爲了維護私有變量不被修改,咱們可使用閉包。node

var math = (function() {
  var _flag = 0;

  return {
    add: function(a, b) {
      return a + b;
    },
    divide: function(a, b) {
      return a / b;
    }
  };
})();

外部代碼只能訪問返回的 adddivide 方法,內部的 _flag 變量是不能訪問的。關於建立對象的一些方法的解釋,能夠參考個人另外一篇博文,裏面有較詳細的解釋。jquery

利用自執行函數的特色,咱們還能夠很方便地爲模塊添加方法:git

var math = (function(module) {
  module.subtract = function(a, b) {
    return a - b;
  }
})(math);

模塊在全局變量中的名稱可能會與其餘的模塊產生衝突,例如 $ 符號,雖然使用方便,但多個模塊可能都會用它做爲本身的簡寫,例如jQuery。咱們能夠在模塊的組織代碼中用 $ 做爲形參,將模塊的全名變量做爲參數傳入,可起到防衝突的效果。github

var math = (function($) {
  // 這裏的$指的就是Math
})(math);

模塊的構建思想即是經過這樣的方式逐漸演化而來,下面將經過介紹一些JS模塊化編程的標準來展現如何組織,管理和編寫模塊。編程

AMDCMD

在JavaScript模塊化編程的世界中,有兩個規範不得不提,它們分別是AMDCMD。如今的JS庫或框架,凡是模塊化的,通常都是遵循了這兩個規範其中之一。

AMD(Asynchronous Module Definition)

CommonJS
在說AMD以前,先要提一下CommonJS。CommonJS是爲了彌補JavaScript標準庫過少的缺點而產生的,因爲JS沒有模塊機制(ES6引入了模塊系統,但瀏覽器全面支持估計還有好幾年),CommonJS就幫助JS實現模塊的功能。如今很熱門的Node.js就是CommonJS規範的一個實現。

CommonJS在模塊中定義方法要藉助一個全局變量 exports,它用來生成當前模塊的API:

/* math module */

exports.add = function(a, b) {
  return a + b;
};

要加載模塊就要使用CommonJS的一個全局方法 require()。加載以前實現的 math 模塊像這樣:

var math = require('math');

加載後 math 變量就是這個模塊對象的一個引用,要調用模塊中的方法就像調用普通對象的方法同樣了:

var math = require('math');
math.add(1, 3);

總之,CommonJS就是一個模塊加載器,能夠方便地對JavaScript代碼進行模塊化管理。但它也有缺點,它在設計之初並無徹底爲瀏覽器環境考慮,瀏覽器環境的特色是全部的資源,不考慮本地緩存的因素,都須要從服務器端加載,加載的速度取決於網絡速度,而CommonJS的模塊加載過程是同步阻塞的。也就是說若是 math 模塊體積很大,網速又很差的時候,整個程序便會中止,等待模塊加載完成。

隨着瀏覽器端JS資源的體積愈來愈龐大,阻塞給體驗帶來的不良影響也愈來愈嚴重,終於從,在CommonJS社區中有了不一樣的聲音,AMD 規範誕生了。

AMD
它的特色即是異步加載,模塊的加載不會影響其餘代碼的運行。全部依賴於某個模塊的代碼所有移到模塊加載語句的回調函數中去。AMD的 require() 語句接受兩個參數:

// require([module], callback)
require(['math'], function(math) {
  math.add(1, 3);
});

在回調函數中,能夠經過 math 變量引用模塊。

AMD規範也規定了模塊的定義規則,使用 define() 函數。

define(id?, dependencies?, factory);

它接受三個參數:
id
這是一個可選參數,至關於模塊的名字,加載器可經過id名加載對應的模塊。若是沒有提供id,加載器會將模塊文件名做爲默認id。

dependencies
可選,接受一個數組參數,傳入當前對象依賴的對象id。

factory
回調函數,在依賴模塊加載完成後會調用,它的參數是全部依賴模塊的引用。回調函數的返回值就是當前對象的導出值。

用AMD規範實現一個簡單的模塊能夠這樣:

define('foo', ['math'], function(math) {
  return {
    increase: function(x) {
      return math.add(x, 1);
    }
  };
});

若是省去id和dependencies參數的話,就是一個徹底的匿名模塊。factory的參數將爲默認值 requireexportsmodule 加載器將徹底經過文件路徑的方式加載模塊,同時若是有依賴模塊的話可經過 require 方法加載。

define(function(require, exports, module) {
  var math = require('math');

  exports.increase = function(x) {
    return math(x, 1);
  };
});

AMD規範也容許對加載進行一些配置,配置選項不是必須的,但靈活更改配置,會給開發帶來一些方便。

baseUrl 以字符串形式規定根目錄的路徑,之後在加載模塊時都會以該路徑爲標準。在瀏覽器中,工做目錄的路徑就是運行腳本的網頁所在的路徑。

{
  baseUrl: './foo/bar'
}

path 能夠指定需加載模塊的路徑,模塊名與路徑以鍵-值對的方式寫在對象中。若是一個模塊有多個可選地址,能夠將這些地址寫在一個數組中。

{
  path: {
    'foo': './bar'
  }
}

關於模塊路徑的設置項還有packagesmap

shim
對於某些沒有按照AMD規範編寫的模塊,好比jQuery,來講,要使它們能被加載器加載,須要用 shim 方法爲其配置一些屬性。在 main 模塊中,用 require.config() 方法:

require.config({
  shim: {
    'jquery': {
      exports: '$'
    },
    'foo': {
      deps: [
        'bar',
        'jquery'
      ],
      exports: 'foo'
    }
  }
});

以後再用加載器加載就能夠了。

目前實現了AMD規範的庫有不少,比較有名的是Require.js

CMD(Common Module Definition)

CMD在不少地方和AMD有類似之處,在這裏我只說二者的不一樣點。

首先,CMD規範和CommonJS規範是兼容的,相比AMD,它簡單不少。遵循CMD規範的模塊,能夠在Node.js中運行。

define
與AMD規範不一樣的是CMD規範中不使用 iddeps 參數,只保留 factory。其中:
1.factory 接收對象/字符串時,代表模塊的接口就是對象/字符串。

define({ 'foo': 'bar' });

define('My name is classicemi.');

define.cmd
其值爲一個空對象,用於判斷頁面中是否有CMD模塊加載器。

if (typeof define === 'function' && define.cmd) {
  // 使用CMD模塊加載器編寫代碼
}

require
此函數一樣用於獲取模塊接口。如需異步加載模塊,使用 require.async 方法。

define(function(require, exports, module) {
  require.async('math', function(math) {
    math.add(1, 2);
  });
});

咱們能夠發現,require(id) 的寫法和CommonJS同樣是以同步方式加載模塊。要像AMD規範同樣異步加載模塊則使用 define.async 方法。

exports
此方法用於模塊對外提供接口。

define(function(require, exports, module) {
  // 對外提供foo屬性
  exports.foo = 'bar';

  // 對外提供add方法
  exports.add = function(a, b) {
    return a + b;
  }
});

提供接口的另外一個方法是直接return包含接口鍵值對的對象:

define(function(require, exports, module) {
  return {
    foo: 'bar',
    add: function(a, b) {
      return a + b;
    }
  }
});

可是注意,不能用exports輸出接口對象:

define(function(require, exports, module) {
  exports = {
    foo: 'bar',
    add: function(a, b) {
      return a + b;
    }
  }
});

這樣寫是錯誤的!
替代方式是這樣寫:

define(function(require, exports, module) {
  module.exports = {
    foo: 'bar',
    add: function(a, b) {
      return a + b;
    }
  }
});

以前錯誤的緣由是在 factory 內部,exports 其實是 module.exports 的一個引用,直接給 exports 賦值是不會改變 module.exports 的值的。

在module對象上,除了有上面提到的 exports 之外,還有一些別的屬性和方法。
module.id
模塊的標識。

define('math', [], function(require, exports, module) {
  // module.id 的值爲 math
});

module.uri
模塊的絕對路徑,由模塊系統解析獲得。

define(function(require, exports, module) {
  console.log(module.uri); // http://xxx.com/path/
});

module.dependencies
值爲一個數組,返回本模塊的依賴。

Require.js 和 Sea.js

以前在說AMD規範的時候提到了Require.js。它是AMD規範的表明性產品。另外一個Sea.js在前端界也是赫赫有名了,CMD規範實際上就是它的產出。它們之間的區別也很能表現AMD和CMD規範之間的區別。

AMD的依賴須要前置書寫

define(['foo', 'bar'], function(foo, bar) {
  foo.add(1, 2);
  bar.subtract(3, 4);
});

CMD的依賴就近書寫便可,不須要提早聲明:
同步式:

define(function(require, exports, module) {
  var foo = require('foo');
  foo.add(1, 2);
  ...
  var bar = require('bar');
  bar.subtract(3, 4);
});

異步式:

define(function(require, exports, module) {
  ...
  require.async('math', function(math) {
    math.add(1, 2);
  });
  ...
});

雖然AMD也能夠用和CMD類似的方法,但不是官方推薦的。

以前在介紹CMD的API時,咱們能夠發現其API職責專注,例如同步加載和異步加載的API都分爲 requirerequire.async,而AMD的API比較多功能。

總而言之,引用玉伯的總結:

  1. Require.js同時適用於瀏覽器端和服務器環境的模塊加載。Sea.js則專一於瀏覽器端的模塊加載實現。經過Node擴展也能夠運行於Node環境中。
  2. Require.js -> AMD,Sea.js -> CMD。
  3. RequireJS 在嘗試讓第三方類庫修改自身來支持 RequireJS,目前只有少數社區採納。Sea.js 不強推,採用自主封裝的方式來「海納百川」,目前已有較成熟的封裝策略。
  4. Sea.js的調試工具比較完備,Require.js調試比較不方便。
  5. RequireJS 採起的是在源碼中預留接口的形式,插件類型比較單一。Sea.js 採起的是通用事件機制,插件類型更豐富。

怎麼看都像是在自詡啊= =,固然它有這個資格

參考文獻

  1. CommonJS官網
  2. 阮一峯博客
  3. AMD Github
  4. CMD Github
  5. Sea.js
  6. Require.js
相關文章
相關標籤/搜索