什么是模塊化
好的代碼模塊分割的內容一定是很合理的,便於你增加減少或者修改功能,同時又不會影響整個系統。
為什么要使用模塊
1.可維護性:根據定義,每個模塊都是獨立的。良好設計的模塊會盡量與外部的代碼撇清關系,以便於獨立對其進行改進和維護。維護一個獨立的模塊比起一團凌亂的代碼來說要輕松很多。
2.命名空間:在JavaScript中,最高級別的函數外定義的變量都是全局變量(這意味着所有人都可以訪問到它們)。也正因如此,當一些無關的代碼碰巧使用到同名變量的時候,我們就會遇到“命名空間污染”的問題。
模塊模式
模塊模式一般用來模擬類的概念(因為原生JavaScript並不支持類,雖然最新的ES6里引入了Class不過還不普及)這樣我們就能把公有和私有方法還有變量存儲在一個對象中——這就和我們在Java或Python里使用類的感覺一樣。這樣我們就能在公開調用API的同時,仍然在一個閉包范圍內封裝私有變量和方法。
實現模塊模式的方法有很多種,下面的例子是通過匿名閉包函數的方法。(在JavaScript中,函數是創建作用域的唯一方式。)
例1:匿名閉包函數
通過這種構造,我們的匿名函數有了自己的作用域或“閉包”。 這允許我們從父(全局)命名空間隱藏變量。
這種方法的好處在於,你可以在函數內部使用局部變量,而不會意外覆蓋同名全局變量,但仍然能夠訪問到全局變量,如下所示:
要注意的是,一定要用括號把匿名函數包起來,以關鍵詞function開頭的語句總是會被解釋成函數聲明(JS中不允許沒有命名的函數聲明),而加上括號后,內部的代碼就會被識別為函數表達式。其實這個也叫作立即執行函數(IIFE)感興趣的同學可以在這里了解更多
例2:全局引入
另一種比較受歡迎的方法是一些諸如jQuery的庫使用的全局引入。和我們剛才舉例的匿名閉包函數很相似,只是傳入全局變量的方法不同:
在這個例子中,globalVariable 是唯一的全局變量。這種方法的好處是可以預先聲明好全局變量,讓你的代碼更加清晰可讀。
例3:對象接口
像下面這樣,還有一種創建模塊的方法是使用獨立的對象接口:
例4:揭示模塊模式 Revealing module pattern
這和我們之前的實現方法非常相近,除了它會確保,在所有的變量和方法暴露之前都會保持私有:
到這里,其實我們只聊了模塊模式的冰山一角。
CommonJS & AMD
上述的所有解決方案都有一個共同點:使用單個全局變量來把所有的代碼包含在一個函數內,由此來創建私有的命名空間和閉包作用域。
接下來介紹兩種廣受歡迎的解決方案:CommonJS 和 AMD.
CommonJS
CommonJS 擴展了JavaScript聲明模塊的API.
CommonJS模塊可以很方便得將某個對象導出,讓他們能夠被其他模塊通過 require 語句來引入。要是你寫過 Node.js 應該很熟悉這些語法。
通過CommonJS,每個JS文件獨立地存儲它模塊的內容(就像一個被括起來的閉包一樣)。在這種作用域中,我們通過 module.exports 語句來導出對象為模塊,再通過 require 語句來引入。
還是舉個直觀的例子吧:
過指定導出的對象名稱,CommonJS模塊系統可以識別在其他文件引入這個模塊時應該如何解釋。
然后在某個人想要調用 myMoudle 的時候,只需要 require 一下:
這種實現比起模塊模式有兩點好處:
-
避免全局命名空間污染
-
明確代碼之間的依賴關系
需要注意的一點是,CommonJS以服務器優先的方式來同步載入模塊,假使我們引入三個模塊的話,他們會一個個地被載入。
它在服務器端用起來很爽,可是在瀏覽器里就不會那么高效了。畢竟讀取網絡的文件要比本地耗費更多時間。只要它還在讀取模塊,瀏覽器載入的頁面就會一直卡着不動。
Asynchronous Module Definition(異步模塊定義規范),簡稱AMD.
通過AMD載入模塊的代碼一般這么寫:
這里我們使用 define 方法,第一個參數是依賴的模塊,這些模塊都會在后台無阻塞地加載,第二個參數則作為加載完畢的回調函數。
回調函數將會使用載入的模塊作為參數。在這個例子里就是 myMoudle 和 myOtherModule.最后,這些模塊本身也需要通過 define 關鍵詞來定義。
拿 myModule 來舉個例子:
UMD
在一些同時需要AMD和CommonJS功能的項目中,你需要使用另一種規范:Universal Module Definition(通用模塊定義規范)。
UMD創造了一種同時使用兩種規范的方法,並且也支持全局變量定義。所以UMD的模塊可以同時在客戶端和服務端使用。
為什么要打包模塊?
一般來講,我們用模塊化組織代碼的時候,都會把模塊划分在不同的文件和文件夾里,也可能會包含一些諸如React和Underscore一類的第三方庫。
而后,所有的這些模塊都需要通過<script>標簽引入到你的HTML文件中,然后用戶在訪問你網頁的時候它才能正常顯示和工作。每個獨立的<script>標簽都意味着,它們要被瀏覽器分別一個個地加載。
為了解決這個問題,我們就需要進行模塊打包,把所有的模塊合並到一個或幾個文件中,以此來減少HTTP請求數。這也可以被稱作是從開發到上線前的構建環節。
還有一種提升加載速度的做法叫做代碼壓縮(混淆)。其實就是去除代碼中不必要的空格、注釋、換行符一類的字符,來保證在不影響代碼正常工作的情況下壓縮其體積。
更小的文件體積也就意味着更短的加載時間。要是你仔細對比過帶有 .min后綴的例如 jquery.min.js和jquery.js的話,應該會發現壓縮版的文件相較之下要小很多。
Gulp和Grunt一類的構建工具可以很方便地解決上述的需求,在開發的時候通過模塊來組織代碼,上線時再合並壓縮提供給瀏覽器。
打包模塊的方法有哪些?
如果你的代碼是通過之前介紹過的模塊模式來組織的,合並和壓縮它們其實就只是把一些原生的JS代碼合在一起而已。
但如果你使用的是一些瀏覽器原生不支持的模塊系統(例如CommonJS 或 AMD,以及ES6 模塊的支持現在也不完整),你就需要使用一些專門的構建工具來把它們轉換成瀏覽器支持的代碼。這類工具就是我們最近經常聽說的Browserify, RequireJS, Webpack等等模塊化構建、模塊化加載工具了。
為了實現模塊化構建或載入的功能,這類工具提供許多諸如在你改動源代碼后自動重新構建(文件監聽)等一系列的功能。
下面我們就一起來看一些實際的例子吧:
打包 CommonJS
在上一篇教程中我們了解到, CommonJS是同步載入模塊的,這對瀏覽器來說不是很理想。其實下面介紹的模塊化構建工具Browserify在上一篇也提到過。它是一個專門用來打包CommonJS模塊以便在瀏覽器里運行的構建工具。
舉個例子,假如你在 main.js 文件中引入了一個用來計算平均數的功能模塊
在這個示例中,我們只有一個名為 myDependency 的模塊依賴。通過下面的命令,Browserify會依次把main.js里引入的所有模塊一同打包到一個名為 bundle.js 的文件里:
Browserify 首先會通過抽象語法樹(AST)來解析代碼中的每一個 require 語句,在分析完所有模塊的依賴和結構之后,就會把所有的代碼合並到一個文件中。然后你在HTML文件里引入一個bundle.js就夠啦。
多個文件和多個依賴也只需要再稍微配置一下就能正常工作了。
之后你也可以使用一些例如Minify-JS的工具來壓縮代碼。
打包 AMD
假若你使用的是AMD,你會需要一些例如RequireJS 或 Curl的AMD加載器。模塊化加載工具可以在你的應用中按需加載模塊代碼。
需要再次提醒一下,AMD 和 CommonJS 的最主要區別是AMD是異步加載模塊的。這也就意味着你不是必須把所有的代碼打包到一個文件里,模塊加載不影響后續語句執行,逐步加載的的模塊也不會導致頁面阻塞無法響應。
不過在實際應用中,為了避免用戶過多的請求對服務器造成壓力。大多數的開發者還是選擇用RequireJS optimizer, r.js一類的構建工具來合並和壓縮AMD的模塊。
總的來說,AMD 和 CommonJS 在構建中最大的區別是,在開發過程中,采用AMD的應用直到正式上線發布之前都不需要構建。
Webpack
Webpack 是新推出的構建工具里最受歡迎的。它兼容CommonJS, AMD, ES6各類規范。
也許你會質疑,我們已經有這么多諸如Browserify 或 RequireJS 的工具了,為什么還需要 Webpack 呢?究其原因之一,Webpack 提供許多例如 code splitting(代碼分割) 的有用功能,它可以把你的代碼分割成一個個的 chunk 然后按需加載優化性能。
舉個例子,要是你的Web應用中的一些代碼只在很少的情況下才會被用到,把它們全都打包到一個文件里是很低效的做法。所以我們就需要 code splitting 這樣的功能來實現按需加載。而不是把那些很少人才會用到的代碼一股腦兒全都下載到客戶端去。
code splitting 只是 Webpack 提供的眾多強大功能之一。當然,網上也為這些模塊化構建工具吵得不可開交
ES6 模塊
ES6模塊和CommonJS, AMD一類規范最主要的區別是,當你載入一個模塊時,載入的操作實際實在編譯時執行的——也就是在代碼執行之前。所以去掉那些不必要的exports導出語句可以優化我們應用的性能。
假設我們有如下一個使用ES6語法,名為 utils.js 的函數:
現在我們也不清楚到底需要這個函數的哪些功能,所以先全部引入到 main.js 中:
之后我們再調用一下 each 函數:
通過 "tree shaken" 之后的 main.js 看起來就像下面這樣:
注意到這里只導出了我們調用過的 each 方法。
再如果我們只調用 filter 方法的話:
你也可以自己在Rollup.js的實時預覽編輯器里做做試驗:live demo and editor
構建ES6模塊
現在我們已經了解到ES6模塊載入的與眾不同了,但我們還沒有聊到底該怎么構建ES6模塊。
因為瀏覽器對ES6模塊的原生支持還不夠完善,所以現階段還需要我們做一些補充工作。
讓ES6模塊在瀏覽器中順利運行的常用方法有以下幾種:
1.使用語法編譯器(Babel或Traceur)來把ES6語法的代碼編譯成ES5或者CommonJS, AMD, UMD等其他形式。然后再通過Browserify 或 Webpack 一類的構建工具來進行構建。
2.使用Rollup.js,這其實和上面差不多,只是Rollup還會捎帶的利用“tree shaking”技術來優化你的代碼。在構建ES6模塊時Rollup優於Browserify或Webpack的也正是這一點,它打包出來的文件體積會更小。Rollup也可以把你的代碼轉換成包括ES6, CommonJS, AMD, UMD, IIFE在內的各種格式。其中IIFE和UMD可以直接在瀏覽器里運行,AMD, CommonJS, ES6等還需要你通過Browserify, Webpack, RequireJS一類的工具才能在瀏覽器中使用。
myModule.js
main.js
同樣,你可以在script標簽上設置type=module的屬性來直接定義模塊:
HTTP/2 出現之后,模塊化構建工具是不是都該被淘汰了?
在HTTP/1中,一次TCP連接只允許一個請求,所以我們需要通過減少載入的文件數來優化性能。而HTTP/2改變了這一切,請求和響應可以並行,一次連接也允許多個請求。
每次請求的消耗也會遠遠小於HTTP/1,所以載入一堆模塊就不再是一個影響性能的問題了。所以許多人認為打包模塊完全就是多余的了。這聽起來很合理,但我們也需要具體情況具體分析。
其中有一條,模塊化構建解決了一些HTTP/2解決不了的問題。例如去除冗余的代碼以壓縮體積。要是你開發的是一個對性能要求很高的網站,模塊化構建從長遠上考慮會給你帶來更多好處。當然,要是你不那么在意性能問題,以后完全就可以省卻這些煩人的步驟了。
總之,我們離所有的網站都采用HTTP/2傳輸還有相當一段時間。短期內模塊化構建還是很有必要的。