談談前端模組化的演變歷程
前言
Javascript不是一種模組化程式語言,它不支援"類"(class),更遑論"模組"(module)了,隨著前端發展對
模組需求越來越大,模組也是經歷了從最初的簡單模組寫法到AMD和CMD規範的出現,再到ES6釋出,目前已經可以
很方便的在Javascript中使用"類"和"模組"了。
一、以前的寫法
1、原始寫法
function m1(){ //... } function m2(){ //... }
缺點 :
"汙染"了全域性變數,無法保證不與其他模組發生變數名衝突,而且模組成員之間看不出直接關係。
2、物件寫法
為了解決上面缺點,把模組寫成一個物件,所有的模組成員都放到這個物件裡面
var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } });
使用
module1.m1();
存在缺點:
但是,這樣的寫法會暴露所有模組成員,內部狀態可以被外部改寫。比如,外部程式碼可以直接改變內部計數器的值。
module1._count = 5;
3、立即執行函式寫法
立即執行函式(IIFE),可以達到不暴露私有成員的目的。
var module1 = (function(){ var _count = 0; var m1 = function(){ //... }; var m2 = function(){ //... }; return { m1 : m1, m2 : m2 }; })();
上面的寫法,外部程式碼無法讀取內部的_count變數。
console.info(module1._count); //undefined
4、放大模式
如果一個模組很大,必須分成幾個部分,或者一個模組需要繼承另一個模組
var module1 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1);
上面的程式碼為module1模組添加了一個新方法m3(),然後返回新的module1模組。
5、寬放大模式
在瀏覽器環境中,模組的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先載入。如果採用上一節的寫法,第一個執行的部分有可能載入一個不存在空物件,這時就要採用"寬放大模式"。
var module1 = ( function (mod){ //... return mod; })(window.module1 || {});
與"放大模式"相比,"寬放大模式"就是"立即執行函式"的引數可以是空物件。
6、輸入全域性變數
獨立性是模組的重要特點,模組內部最好不與程式的其他部分直接互動。
為了在模組內部呼叫全域性變數,必須顯式地將其他變數輸入模組
var module1 = (function ($, YAHOO) { //... })(jQuery, YAHOO);
上面的module1模組需要使用jQuery庫和YUI庫,就把這兩個庫(其實是兩個模組)當作引數輸入module1
好處:
這樣做除了保證模組的獨立性,還使得模組之間的依賴關係變得明顯
二、CommonJS、AMD和CMD的出現
談到AMD和CMD,不得不說下CommonJS。2009年,美國程式設計師Ryan Dahl創造了node.js專案,將javascript語言用於伺服器端程式設計,
由於瀏覽器端網頁還比較簡單 ,對於模組不是特別依賴,但在伺服器端因為要與作業系統和其他應用程式互動,CommonJS
就在這樣的背景下誕生了。
引入第三方模組並呼叫方法
var math = require('math'); math.add(2,3); // 5
這裡有一個CommonJS模組使用的例子
//模組定義 myModule.js var name = 'Byron'; function printName(){ console.log(name); } function printFullName(firstName){ console.log(firstName + name); } module.exports = { printName: printName, printFullName: printFullName } //載入模組 var myModule = require('./myModule.js'); myModule.printName();
1、AMD規範
想象一下如果把這段程式碼放到瀏覽器端 ,math.add(2, 3),在第一行require('math')之後執行,因此必須等math.js載入完成。也就是說,如果載入時間很長,整個應用就會停在那裡等
var math = require('math'); math.add(2, 3);
這對伺服器端不是一個問題,因為所有的模組都存放在本地硬碟,可以同步載入完成,等待時間就是硬碟的讀取時間。但是,對於瀏覽器,這卻是一個大問題,因為模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態
因此,瀏覽器端的模組,不能採用"同步載入"(synchronous),只能採用"非同步載入"(asynchronous)
AMD也採用require()語句載入模組,但是不同於CommonJS,它要求兩個引數
require([module], callback);
上面程式碼改寫成AMD形式就是這樣
require(['math'], function (math) { math.add(2, 3); });
目前實現AMD規範的有RequireJS和curl.js
RequireJS模組例子:
// 定義模組 myModule.js define('myModule', ['dependency'], function(){ var name = 'Byron'; function printName(){ console.log(name); } return { printName: printName }; }); // 載入模組 require(['myModule'], function (my){ my.printName(); });
2、CMD規範
CMD: Common Module Definition通用模組定義, 由國內發展出來, SeaJS是其典型代表, 即SeaJS是通過瀏覽器對CMD的具體實現
SeaJS模組例子:
// 定義模組myModule.js define(function(require, exports, module) { var $ = require('jquery.js'); var foo = require('foo'); var out = foo.bar(); $('div').addClass('active'); module.exports = out; }); // 載入模組 seajs.use(['myModule.js'], function(my){ });
3、CommonJS、AMD和CMD區別
- CommonJS是同步的, 主要用於伺服器
- AMD和CMD是非同步的, 兩者的模組定義和載入機制稍有不同, 主要用於瀏覽器
-
AMD推崇依賴前置,在定義模組的時候就要宣告其依賴的模組,CMD`推崇就近依賴,只有在用到某個模組的時候再去require
兩個都是定義的全域性define函式來定義模組, define接收函式function(require, exports, module)保持一致 - CMD是懶載入, 僅在require時才會載入模組; - AMD是預載入, 在定義模組時就提前載入好所有依賴
- CMD保留了CommonJS風格
三、ES6的使用
ES6在語言標準的層面上, 實現了模組功能, 而且實現得相當簡單, 完全可以取代CommonJS和AMD規範, 是瀏覽器和伺服器通用的模組解決方案
ES6模組例子:
//模組定義 myModule.js const name = 'Byron'; function printName(){ console.log(name); } function printFullName(firstName){ console.log(firstName + name); } const myModule = { printName: printName, printFullName: printFullName }; export myModule; //載入模組 import myModule, { printFullName } from './myModule.js'; myModule.printName(); printFullName('Michael');
四、最後再說說browserify和webpack
說到 browserify / webpack ,那還要說到 seajs / requirejs 。這四個都是JS模組化的方案。其中seajs / require 是一種型別,browserify / webpack 是另一種型別。
1、seajs / require :
是一種線上"編譯"模組的方案,相當於在頁面上載入一個 CMD/AMD 直譯器。這樣瀏覽器就認識了 define、exports、module 這些東西。也就實現了模組化。
2、browserify / webpack : 是一個預編譯模組的方案,相比於上面 ,這個方案更加智慧。沒用過browserify,這裡以webpack為例。首先,它是預編譯的,不需要在瀏覽器中載入直譯器。另外,你在本地直接寫JS,不管是 AMD / CMD / ES6 風格的模組化,它都能認識,並且編譯成瀏覽器認識的JS。