Brendan Eich用了10天就創造了JavaScript,由於當時的需求定位,致使了在設計之初,在語言層就不包含不少高級語言的特性,其中就包括模塊這個特性,可是通過了這麼多年的發展,現在對JavaScript的需求已經遠遠超出了Brendan Eich的預期,其中模塊化開發更是其中最大的需求之一。javascript
尤爲是2009年Node.js出現之後,CommonJS規範的落地極大的推進了整個社區的模塊化開發氛圍,而且隨之出現了AMD、CMD、UMD等等一系列能夠在瀏覽器等終端實現的異步加載的模塊化方案。html
此前,雖然本身也一直在推動模塊化開發,可是沒有深刻了解過模塊化演進的歷史,直到最近看到了一篇文章《精讀JS模塊化發展》,文章總結了History of JavaScript這個開源項目中關於JavaScript模塊化演進的部分,細讀幾回以後,對於一些之前模棱兩可的東西,頓時清晰了很多,下面就以時間線總結一下本身的理解:前端
在1999年的時候,絕大部分工程師作JS開發的時候就直接將變量定義在全局,作的好一些的或許會作一些文件目錄規劃,將資源歸類整理,這種方式被稱爲直接定義依賴,舉個例子:java
// greeting.js var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; function writeHello(lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write('The script is broken'); } // index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Basic example</title> <script src="./greeting.js"></script> <script src="./third_party_script.js"></script> </head> <body onLoad="writeHello('ru')"> </body> </html>
可是,即便有規範的目錄結構,也不能避免由此而產生的大量全局變量,這就致使了一不當心就會有變量衝突的問題,就比如上面這個例子中的writeHello
。node
因而在2002年左右,有人提出了命名空間模式的思路,用於解決遍地的全局變量,將須要定義的部分歸屬到一個對象的屬性上,簡單修改上面的例子,就能實現這種模式:git
// greeting.js var app = {}; app.helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; app.writeHello = function (lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write('The script is broken'); }
不過這種方式,毫無隱私可言,本質上就是全局對象,誰均可以來訪問而且操做,一點都不安全。es6
因此在2003年左右就有人提出利用IIFE結合Closures特性,以此解決私有變量的問題,這種模式被稱爲閉包模塊化模式:github
// greeting.js var greeting = (function() { var module = {}; var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!', }; module.getHello = function(lang) { return helloInLang[lang]; }; module.writeHello = function(lang) { document.write(module.getHello(lang)); }; return module; })();
IIFE能夠造成一個獨立的做用域,其中聲明的變量,僅在該做用域下,從而達到實現私有變量的目的,就如上面例子中的helloInLang
,在該IIFE外是不能直接訪問和操做的,能夠經過暴露一些方法來訪問和操做,好比說上面例子裏面的getHello
和writeHello
2個方法,這就是所謂的Closures。瀏覽器
同時,不一樣模塊之間的引用也能夠經過參數的形式來傳遞:安全
// x.js // @require greeting.js var x = (function(greeting) { var module = {}; module.writeHello = function(lang) { document.write(greeting.getHello(lang)); }; return module; })(greeting);
此外使用IIFE,還有2個好處:
在那個年代,除了這種解決思路之外,還有經過其它語言的協助來完成模塊化的解決思路,好比說模版依賴定義、註釋依賴定義、外部依賴定義等等,不過不常見,因此就不細說了,究其本源,它們想最終實現的方式都差很少。
不過,這些方案,雖然解決了依賴關係的問題,可是沒有解決如何管理這些模塊,或者說在使用時清晰描述出依賴關係,這點仍是沒有被解決,能夠說是少了一個管理者。
沒有管理者的時候,在實際項目中,得手動管理第三方的庫和項目封裝的模塊,就像下面這樣把全部須要的JS文件一個個按照依賴的順序加載進來:
<script src="zepto.js"></script> <script src="jhash.js"></script> <script src="fastClick.js"></script> <script src="iScroll.js"></script> <script src="underscore.js"></script> <script src="handlebar.js"></script> <script src="datacenter.js"></script> <script src="deferred.js"></script> <script src="util/wxbridge.js"></script> <script src="util/login.js"></script> <script src="util/base.js"></script> <script src="util/city.js"></script>
若是頁面中使用的模塊數量愈來愈多,恐怕再有經驗的工程師也很難維護好它們之間的依賴關係了。
因而如LABjs之類的加載工具就橫空出世了,經過使用它的API,動態建立<script>
,從而達到控制JS文件加載以及執行順序的目的,在必定的程度上解決了依賴關係,例如:
$LAB.script("greeting.js").wait() .script("x.js") .script("y.js").wait() .script("run.js");
不過LABjs之類的加載工具是創建在以文件爲單位的基礎之上的,可是JS中的模塊又不必定必須是文件,同一個文件中能夠聲明多個模塊,YUI做爲昔日前端領域的佼佼者,很好的糅合了命名空間模式及沙箱模式,下面來一睹它的風采:
// YUI - 編寫模塊 YUI.add('dom', function(Y) { Y.DOM = { ... } }) // YUI - 使用模塊 YUI().use('dom', function(Y) { Y.DOM.doSomeThing(); // use some methods DOM attach to Y }) // hello.js YUI.add('hello', function(Y){ Y.sayHello = function(msg){ Y.DOM.set(el, 'innerHTML', 'Hello!'); } },'3.0.0',{ requires:['dom'] }) // main.js YUI().use('hello', function(Y){ Y.sayHello("hey yui loader"); })
此外,YUI團隊還提供的一系列用於JS壓縮、混淆、請求合併(合併資源須要server端配合)等性能優化的工具,說其是現有JS模塊化的鼻祖一點都不過度。
不過,隨着Node.js的到來,CommonJS規範的落地以及各類前端工具、解決方案的出現,很快,YUI3就被湮沒在了歷史的長流裏面,這樣成爲了JS模塊化開發的一個分水嶺,引用一段描述:
從 1999 年開始,模塊化探索都是基於語言層面的優化,真正的革命從 2009 年 CommonJS 的引入開始,前端開始大量使用預編譯。
CommonJS是一套同步的方案,它考慮的是在服務端運行的Node.js,主要是經過require
來加載依賴項,經過exports
或者module.exports
來暴露接口或者數據的方式,想了解更多,能夠看一下《CommonJS規範》,下面舉個簡單的例子:
var math = require('math'); esports.result = math.add(2,3); // 5
因爲服務器上經過require
加載資源是直接讀取文件的,所以中間所需的時間能夠忽略不計,可是在瀏覽器這種須要依賴HTTP獲取資源的就不行了,資源的獲取所需的時間不肯定,這就致使必須使用異步機制,表明主要有2個:
它們分別在瀏覽器實現了define
、require
及module
的核心功能,雖然二者的目標是一致的,可是實現的方式或者說是思路,仍是有些區別的,AMD偏向於依賴前置,CMD偏向於用到時才運行的思路,從而致使了依賴項的加載和運行時間點會不一樣,關於這2者的比較,網上有不少了,這裏推薦幾篇僅供參考:
本人就先接觸了SeaJS後轉到RequireJS,雖然感受AMD的模式寫確實沒有CMD這麼符合一慣的語義邏輯,可是寫了幾個模塊之後就習慣了,並且社區資源比較豐富的AMD陣營更加符合當時的項目需求(扯多了),下面分別寫個例子作下直觀的對比:
// CMD define(function (require) { var a = require('./a'); // <- 運行到此處纔開始加載並運行模塊a var b = require('./b'); // <- 運行到此處纔開始加載並運行模塊b // more code .. })
// AMD define( ['./a', './b'], // <- 前置聲明,也就是在主體運行前就已經加載並運行了模塊a和模塊b function (a, b) { // more code .. } )
經過例子,你能夠看到除了語法上面的區別,這2者主要的差別仍是在於:
什麼時候加載和運行依賴項?
這也是CommonJS社區中質疑AMD最主要緣由之一,很多人認爲它破壞了規範,反觀CMD模式,簡單的去除define
的外包裝,這就是標準的CommonJS實現,因此說CMD是最貼近CommonJS的異步模塊化方案,不過孰優孰劣,這裏就不扯了,需求決定一切。
此外同一時期還出現了一個UMD的方案,其實它就是AMD與CommonJS的集合體,經過IIFE的前置條件判斷,使一個模塊既能夠在瀏覽器運行,也能夠在Node.JS中運行,舉個例子:
// UMD (function(define) { define(function () { var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; return { sayHello: function (lang) { return helloInLang[lang]; } }; }); }( typeof module === 'object' && module.exports && typeof define !== 'function' ? function (factory) { module.exports = factory(); } : define ));
我的以爲最少用到的就是這個UMD模式了。
2015年6月,ECMAScript2015也就是ES6發佈了,JavaScript終於在語言標準的層面上,實現了模塊功能,使得在編譯時就能肯定模塊的依賴關係,以及其輸入和輸出的變量,不像 CommonJS、AMD之類的須要在運行時才能肯定(例如FIS這樣的工具只能預處理依賴關係,本質上仍是運行時解析),成爲瀏覽器和服務器通用的模塊解決方案。
// lib/greeting.js const helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; export const getHello = (lang) => ( helloInLang[lang]; ); export const sayHello = (lang) => { console.log(getHello(lang)); }; // hello.js import { sayHello } from './lib/greeting'; sayHello('ru');
與CommonJS用require()
方法加載模塊不一樣,在ES6中,import
命令能夠具體指定加載模塊中用export
命令暴露的接口(不指定具體的接口,默認加載export default
),沒有指定的是不會加載的,所以會在編譯時就完成模塊的加載,這種加載方式稱爲編譯時加載或者靜態加載。
而CommonJS的require()
方法是在運行時才加載的:
// lib/greeting.js const helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; const getHello = function (lang) { return helloInLang[lang]; }; exports.getHello = getHello; exports.sayHello = function (lang) { console.log(getHello(lang)) }; // hello.js const sayHello = require('./lib/greeting').sayHello; sayHello('ru');
能夠看出,CommonJS中是將整個模塊做爲一個對象引入,而後再獲取這個對象上的某個屬性。
所以ES6的編譯時加載,在效率上面會提升很多,此外,還會帶來一些其它的好處,好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。
惋惜的是,目前瀏覽器和Node.js的支持程度都並不理想,截止發稿,也就只有 Chrome61+ 與 Safari10.1+ 才作到了部分支持。
不過能夠經過Babel這類工具配合相關的plugin(能夠參考《Babel筆記》),轉換爲ES5的語法,這樣就能夠在Node.js運行起來了,若是想在瀏覽器上運行,能夠添加Babel配置,爲模塊文件添上AMD的define
函數做爲外層,再並配合RequireJS之類的加載器便可。
更多關於ES6 Modules的資料,能夠看一下《ECMAScript 6 入門 - Module 的語法》。
本文先發佈於個人我的博客《 JavaScript模塊化開發的演進歷程》,後續若有更新,能夠查看原文。