凡是開發大型應用程序,模塊塊必然是不可或缺的一部分。那麼什麼是模塊化呢?其實模塊化就是將一個複雜的系統分解成多個獨立的模塊的代碼組織方式。在很長的一段時間裏,前端只能通過一系列的<script>
標籤來維護我們的代碼關係,但是一旦我們的項目複雜度提高的時候,這種簡陋的代碼組織方式便是如噩夢般使得我們的代碼變得混亂不堪。所以,在開發大型Javascript應用程序的時候,就必須引入模塊化機制。由於早期官方並沒有提供統一的模塊化解決方案,所以在羣雄爭霸的年代,各種前端模塊化方案層出不窮。本文將從最早期的IFEE閉包方案到現在的ES6 Modules, 追根溯源,帶你詳細瞭解前端模塊化的前世今生。
IIFE
模塊化的一大作用就是用來隔離作用域,避免變量衝突。而Javascript沒有語言層面的命名空間概念,只能將代碼暴露到全局作用域下。在刀耕火種的年代,作爲腳本語言的Javascript爲了避免全局變量污染,只能使用閉包來實現模塊化。好在我們可以利用自執行函數(IIFE)來執行代碼,從而避免變量名泄漏到全局作用域中:
(function(window) {
window.jQuery = {
// 這裏是代碼
};
})(window);
雖然IIFE可以有效解決命名衝突的問題,但是對於依賴管理,還是束手無策。由於瀏覽器是從上至下執行腳本,因此爲了維持腳本間的依賴關係,就必須手動維護好script
標籤的相對順序。
AMD
AMD (Asynchronous Module Definition)也是一種 JavaScript模塊化規範。從名字上可以看出,它主要提供了異步加載的功能。對於多個JS模塊之間的依賴問題,如果使用原生的方式加載代碼,隨着加載文件的增多,瀏覽器會長時間地失去響應,而AMD能夠保證被依賴的模塊儘早地加載到瀏覽器中,從而提高頁面響應速度。由於該規範原生Javascript無法支持,所以必須使用相應的庫來實現對應的模塊化。RequireJS就是實現了該規範的類庫,實際上AMD也是其在推廣過程中的產物。
利用RequireJS來編寫模塊,所有的依賴項必須提前聲明好。在導入模塊的時候,也會先加載對應的依賴模塊,然後再執行接下來的代碼,同時AMD模塊可以並行加載所有依賴模塊,從而很好地提高頁面加載性能:
define('./index.js',function(code){
// code 就是index.js 返回的內容
return {
sayHello: function(name) {
return "Hello, " + name;
}
}
});
CMD
CMD(Common Module Definition)最初是由阿里的玉伯提出的,同AMD類似,使用CMD模塊也需要使用對應的庫SeaJS。SeaJS所要解決的問題和requireJS一樣,但是在使用方式和加載時機上有所不同:
define(function(require) {
//通過require引用模塊
var path=require.resolve('./cmdDefine');
alert(path);
});
CMD加載完某個依賴模塊後並不執行,只是下載而已,在所有依賴模塊加載完成後進入主邏輯,遇到require語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是完全一致的。如果使用require.async()方法,可以實現模塊的懶加載。
CommonJS
隨着Javasript應用進軍服務器端,業界急需一種標準的模塊化解決方案,於是,CommonJS(www.commonjs.org)應運而生。它最初是由Kevin Dangoor在他的這篇博文中首次提出。
這是一種被廣泛使用的Javascript模塊化規範,大家最熟悉的Node.js應用中就是採用這個規範。在Node.js中,內置了module對象用來定義模塊, require函數用來加載模塊文件,代碼如下:
// utils.js 模塊定義
var add = function(a, b) {
return a + b;
};
module.exports = {
add: add
};
// 加載模塊
var utils = require('./utils');
console.log(utils.add(1, 2));
此種模塊化方案特點就是:同步阻塞式加載,無法實現按需異步加載。另外,如果想要在瀏覽器中使用CommonJS模塊就需要使用Browserify
進行解析:
npm install browserify -g
browserify utils.js > bundle.js
當然,你也可以使用gulp, webpack等工具進行解析打包後引入到瀏覽器頁面中去。
UMD
上面介紹的CommonJS和AMD等模塊化方案都是針對特定的平臺,如果想要實現跨平臺的模塊化,就得引入UMD的模塊化方式。UMD是通用模塊定義(Universal Module Definition)的縮寫,使用該中模塊化方案,可以很好地兼容AMD, CommonJS等模塊化語法。
接下來,讓我們通過一個簡單地例子看一下如何使用和定義UMD模塊:
(function(root, factory) {
if(typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else if(typeof module === 'object' &&
typeof module.exports === 'object') {
var jquery = require('jquery');
module.exports = factory(jquery);
} else {
root.UmdModule = factory(root.jQuery);
}
}(this, function(jquery) {
// 現在你可以利用jquery做你想做的事了
}));
這種模塊定義方法,可以看做是IIFE的變體。不同的是它倒置了代碼的運行順序,需要你將所需運行的函數作爲第二個參數傳入。由於這種通用模塊的適用性強,很多JS框架和類庫都會打包成這種形式的代碼。
ES6 Modules
對於ES6來說,不必再使用閉包和封裝函數等方式進行模塊化支持了。在ES6中,從語法層面就提供了模塊化的功能。然而受限於瀏覽器的實現程度,如果想要在瀏覽器中運行,還是需要通過Babel等轉譯工具進行編譯。ES6提供了import
和export
命令,分別對應模塊的導入和導出功能。具體實例如下:
// demo-export.js 模塊定義
var name = "scq000"
var sayHello = (name) => {
console.log("Hi," + name);
}
export {name, sayHello};
// demo-import.js 使用模塊
import {sayHello} from "./demo-export";
sayHello("scq000");
對於具體的語法細節,想必大家在日常使用過程中都已經輕車熟路了。但對於ES6模塊化來說,有以下幾點特性是需要記住的:
- ES6使用的是基於文件的模塊。所以必須一個文件一個模塊,不能將多個模塊合併到單個文件中去。
- ES6模塊API是靜態的,一旦導入模塊後,無法再在程序運行過程中增添方法。
- ES6模塊採用引用綁定(可以理解爲指針)。這點和CommonJS中的值綁定不同,如果你的模塊在運行過程中修改了導出的變量值,就會反映到使用模塊的代碼中去。所以,不推薦在模塊中修改導出值,導出的變量應該是靜態的。
- ES6模塊採用的是單例模式,每次對同一個模塊的導入其實都指向同一個實例。
Webpack中的模塊化方案
作爲現代化的前端構建工具,Webpack還提供了豐富的功能能夠使我們更加輕易地實現模塊化。利用Webpack,你不僅可以將Javascript文件進行模塊化,同時還能針對圖片,css等靜態資源進行模塊化。你可以在代碼裏使用CommonJS, ES6等模塊化語法,打包的時候你也可以根據需求選擇打包類型,如UMD, AMD等:
module.exports = {
//...
output: {
library: 'librayName',
libraryTarget: 'umd', // 配置輸出格式
filename: 'bundle.js'
}
};
另外,ES6模塊好處很多,但是並不支持按需加載的功能, 而按需加載又是Web性能優化中重要的一個環節。好在我們可以藉助Webpack來彌補這一缺陷。Webpack v1版本提供了require.ensure
API, 而2.x之後使用了import()
函數來實現異步加載。具體的代碼示例可以查看我之前所寫的前端性能優化之加載技術 這篇文章。
總結
模塊化方案解決了代碼之間錯綜複雜的依賴關係,不僅降低了開發難度同時也讓開發者將精力更多地集中在業務開發中。隨着ES6標準的推出,模塊化直接成爲了Javascript語言規範中的一部分。這也意味着CommonJS, AMD, CMD等模塊化方案終將退出歷史的舞臺。當然,要實現完全ES6模塊化的使用,還需要一段長時間的等待。那麼,在這段過渡的時間裏,我們可能仍然需要維護舊有的代碼,使用傳統的模塊化方案來構建應用。對於前端工程師來說,系統地瞭解主流的模塊化方案就顯得十分必要了。最後,讓我們再一次回顧一下各種模塊化方式的特點:
模塊化方案 | 加載 | 同步/異步 | 瀏覽器 | 服務端 | 模塊定義 | 模塊引入 |
---|---|---|---|---|---|---|
IFEE | 取決於代碼 | 取決於代碼 | 支持 | 支持 | IFEE | 命名空間 |
AMD | 提前預加載 | 異步 | 支持 | 構建工具r.js | define | require |
CMD | 按需加載 | 延遲執行 | 支持 | 構建工具spm | define | define |
Common | 值拷貝,運行時加載 | 同步 | 原生不支持,需要使用browserify提前打包編譯 | 原生支持 | module.exports | require |
UMD | 取決於代碼 | 取決於代碼 | 支持 | 支持 | IFEE | 命名空間 |
ES Modules(ES6) | 實時綁定,動態綁定,編譯時輸出 | 同步 | 需用babel轉譯 | 需用babel轉譯 | export | import |
參考資料
http://javascript.ruanyifeng.com/nodejs/module.html
《你不知道的Javascript》
——本文首發於個人公衆號,轉載請註明出處———
最後,歡迎大家關注我的公衆號,一起學習交流。