你是否是 JavaScript 新手?并且對(duì)模塊,模塊加載器和模塊打包器感到困惑嗎?或者你已經(jīng)編寫(xiě)了一段時(shí)間的 JavaScript ,但是沒(méi)法掌握模塊的一些術(shù)語(yǔ)?你是否聽(tīng)過(guò) CommonJS、AMD、Browserify、SystemJS、Webpack、JSPM 等等術(shù)語(yǔ),但是不理解我們?yōu)槭裁葱枰鼈儯?/p>
我會(huì)試著解釋他們是什么,他們?cè)噲D解決什么問(wèn)題,以及他們?nèi)绾谓鉀Q這個(gè)問(wèn)題。
示例應(yīng)用程序

(該圖為應(yīng)用程序運(yùn)行界面)
在這篇文章中,我將使用一個(gè)簡(jiǎn)單的 web 應(yīng)用程序來(lái)演示模塊的概念。應(yīng)用程序在瀏覽器中顯示數(shù)組的和。該應(yīng)用程序由4個(gè)函數(shù)和一個(gè) index.html 文件組成。

(該圖為函數(shù)的依賴示意圖)
main 函數(shù)計(jì)算數(shù)組中數(shù)字的和,然后把答案顯示在 span#answer 中。sum 函數(shù)依賴于兩個(gè)函數(shù):add 和 reduce。add 函數(shù)做它名字所做的操作;把兩個(gè)數(shù)相加。reduce 函數(shù)遍歷數(shù)組,并且調(diào)用 iteratee 回調(diào)函數(shù)。
花點(diǎn)時(shí)間理解下面的代碼。我將會(huì)重復(fù)多次使用相同的函數(shù)。
1.
2.<!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
13. </body>
14. </html>
1. // 1-main.js
2. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
3. var answer = sum(values)
4. document.getElementById("answer").innerHTML = answer;
JavaScript 代碼:
1. // 2-sum.js
2. function sum(arr){
3. return reduce(arr, add);
4. }
JavaScript 代碼:
1. // 3-add.js
2. function add(a, b) {
3. return a + b;
4. }
JavaScript 代碼:
1. // 4-reduce.js
2. function reduce(arr, iteratee) {
3. var index = 0,
4. length = arr.length,
5. memo = arr[index];
6. for(index += 1; index < length; index += 1){
7. memo = iteratee(memo, arr[index])
8. }
9. return memo;
10. }
我們來(lái)看看如何把這些代碼片段整合在一起,來(lái)構(gòu)建一個(gè)應(yīng)用程序。
使用內(nèi)嵌腳本
內(nèi)嵌腳本就是在 <script></script> 標(biāo)記之間添加 JavaScript 代碼。這是我開(kāi)始學(xué) JavaScript 時(shí)的做法。我相信大多數(shù) JavaScript 開(kāi)發(fā)者在其生命中至少做過(guò)一次這樣的事情。
這是一個(gè)很好的入門(mén)辦法。沒(méi)有外部文件或依賴關(guān)系需要擔(dān)心。但是這也導(dǎo)致了不可維護(hù)的代碼,因?yàn)椋?/p>
- 缺乏代碼可重用性:如果需要添加另一個(gè)頁(yè)面,需要從這個(gè)頁(yè)面中獲得一些函數(shù),我們就不得不復(fù)制粘貼代碼。
- 缺乏依賴解析:你必須保證 main 函數(shù)之前已經(jīng)添加了 add、reduce 和 sum 函數(shù)。
- 全局命名空間污染:所有的函數(shù)和變量將都將駐留在全局作用域中。
HTML 代碼:
1. <!-- index.html -->
2. <!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
14. <script type="text/javascript">
15. function add(a, b) {
16. return a + b;
17. }
18. function reduce(arr, iteratee) {
19. var index = 0,
20. length = arr.length,
21. memo = arr[index];
22. for(index += 1; index < length; index += 1){
23. memo = iteratee(memo, arr[index])
24. }
25. return memo;
26. }
27. function sum(arr){
28. return reduce(arr, add);
29. }
30. /* Main Function */
31. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
32. var answer = sum(values)
33. document.getElementById("answer").innerHTML = answer;
34. </script>
35. </body>
36. </html>
Script 標(biāo)簽引入 JavaScript 文件
這是從嵌入腳本的一個(gè)自然過(guò)渡?,F(xiàn)在我們將大段的 JavaScript 分成更小的代碼片段,并用 <script src=“...”> 標(biāo)簽加載它們。
通過(guò)將文件分成多個(gè) JavaScript 文件,我們可以重用這些代碼。我們不再需要在不同的 html 頁(yè)面之間復(fù)制和粘貼代碼。我們只需要將該文件用 script 標(biāo)簽加載就可以了。盡管這是更好的方法,但仍然有以下問(wèn)題:
-
缺乏依賴解析:文件的順序很重要。你必須保證在加載
main.js文件之前已經(jīng)加載了add.js、reduce.js和sum.js文件。 - 全局命令空間污染:所有的函數(shù)和變量依然在全局作用域中。
HTML 代碼:
1. <!-- 0-index.html -->
2. <!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
14. <script type="text/javascript" src="./add.js"></script>
15. <script type="text/javascript" src="./reduce.js"></script>
16. <script type="text/javascript" src="./sum.js"></script>
17. <script type="text/javascript" src="./main.js"></script>
18. </body>
19. </html>
JavaScript 代碼:
1. //add.js
2. function add(a, b) {
3. return a + b;
4. }
JavaScript 代碼:
1. //reduce.js
2. function reduce(arr, iteratee) {
3. var index = 0,
4. length = arr.length,
5. memo = arr[index];
7. index += 1;
8. for(; index < length; index += 1){
9. memo = iteratee(memo, arr[index])
10. }
11. return memo;
12. }
JavaScript 代碼:
1. //sum.js
2. function sum(arr){
3. return reduce(arr, add);
4. }
JavaScript 代碼:
1. // main.js
2. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
3. var answer = sum(values)
4. document.getElementById("answer").innerHTML = answer;
模塊對(duì)象和 IIFE(模塊模式)
通過(guò)使用模塊對(duì)象和 立即調(diào)用的函數(shù)表達(dá)式(IIFE) ,我們可以減少對(duì)全局作用域的污染。在這種方法中,我們只向全局作用域公開(kāi)一個(gè)對(duì)象。該對(duì)象包含了我們?cè)趹?yīng)用程序中需要的所有方法和值。在本例中,我們只向全局作用域公開(kāi)了 myApp 對(duì)象。所有的函數(shù)都將被保存在 myApp 對(duì)象中。
JavaScript 代碼:
1. // 01-my-app.js
2. var myApp = {};
JavaScript 代碼:
1. // 02-add.js
2. (function(){
3. myApp.add = function(a, b) {
4. return a + b;
5. }
6. })();
JavaScript 代碼:
1. // 03-reduce.js
2. (function(){
3. myApp.reduce = function(arr, iteratee) {
4. var index = 0,
5. length = arr.length,
6. memo = arr[index];
8. index += 1;
9. for(; index < length; index += 1){
10. memo = iteratee(memo, arr[index])
11. }
12. return memo;
13. }
14. })();
JavaScript 代碼:
1. // 04-sum.js
2. (function(){
3. myApp.sum = function(arr){
4. return myApp.reduce(arr, myUtil.add);
5. }
6. })();
JavaScript 代碼:
1. // 05-main.js
2. (function(app){
3. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
4. var answer = app.sum(values)
5. document.getElementById("answer").innerHTML = answer;
6. })(myApp);
HTML 代碼:
1. <!-- 06-index.html -->
2. <!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
14. <script type="text/javascript" src="./my-app.js"></script>
15. <script type="text/javascript" src="./add.js"></script>
16. <script type="text/javascript" src="./reduce.js"></script>
17. <script type="text/javascript" src="./sum.js"></script>
18. <script type="text/javascript" src="./main.js"></script>
19. </body>
20. </html>
</pre>
請(qǐng)注意,除了 my-app.js 之外,其它每個(gè)文件都被封裝成了 IIFE 格式。
JavaScript 代碼:
1. // 立即調(diào)用的函數(shù)表達(dá)式(IIFE) 格式
2. (function(){ /*... your code goes here ...*/ })();
通過(guò)將每個(gè)文件封裝到 IIFE 中,所有的本地變量都保留在函數(shù)作用域內(nèi)。因此,函數(shù)中的所有變量都將保持在函數(shù)作用域內(nèi),而不會(huì)污染全局作用域。
我們通過(guò)將它們附加到myApp對(duì)象來(lái)公開(kāi)添加、減少和sum函數(shù)。我們通過(guò)引用myApp對(duì)象來(lái)訪問(wèn)這些函數(shù)
我們通過(guò)將 add、reduce 和 sum 函數(shù)附加在 myApp 對(duì)象上,從而對(duì)外公開(kāi)它們。我們通過(guò)引用 myApp 對(duì)象來(lái)訪問(wèn)這些函數(shù):
1. myApp.add(1,2);
2. myApp.sum([1,2,3,4]);
3. myApp.reduce(add, value);
我們還可以通過(guò) IIFE 的參數(shù),傳遞 myApp 全局對(duì)象,就像 main.js 文件中所示一樣。通過(guò)將該對(duì)象作為參數(shù)傳遞給 IIFE ,我們就可以為該對(duì)象選擇一個(gè)較短的別名。而我們的代碼會(huì)更簡(jiǎn)潔一些。
JavaScript 代碼:
1. (function(obj){
2. // obj is new veryLongNameOfGlobalObject
3. })(veryLongNameOfGloablObject);
與前面的例子相比,IIFE 是一個(gè)巨大的改進(jìn)。大多數(shù)流行的 JavaScript 庫(kù),如 jQuery ,都使用這種模式。它公開(kāi)了一個(gè)全局對(duì)象 $,所有的函數(shù)都在 $ 對(duì)象中。
然而,這并不能算是一個(gè)完美的解決方案。這種方法仍然面臨與上一節(jié)相同的問(wèn)題。
-
缺乏依賴解析:文件的順序依然重要,
myApp.js必須出現(xiàn)在所有其它文件之前加載,main.js必須處在所有其它庫(kù)文件之后。 - 全局命令空間污染:現(xiàn)在全局變量的數(shù)量變成了 1,但是還不是 0 。
CommonJS
2009年,有人討論將 JavaScript 引入服務(wù)器端。因此 ServerJS 誕生了。隨后,ServerJS 將其名稱改為 CommonJS 。
CommonJS 不是一個(gè) JavaScript 庫(kù)。它是一個(gè)標(biāo)準(zhǔn)化組織。它就像 ECMA 或 W3C 一樣。ECMA 定義了 JavaScript 的語(yǔ)言規(guī)范。W3C定義了 JavaScript web API ,比如 DOM 或 DOM 事件。 CommonJS 的目標(biāo)是為 web 服務(wù)器、桌面和命令行應(yīng)用程序定義一套通用的 API 。
CommonJS 還定義了模塊 API 。因?yàn)樵诜?wù)器應(yīng)用程序中沒(méi)有 HTML 頁(yè)面和 </script><script> 標(biāo)簽,所以為模塊提供一些清晰的 API 是很有意義的。模塊需要被公開(kāi)(**export**)以供其它模塊使用,并且可以訪問(wèn)(**import**)。它的導(dǎo)出模塊語(yǔ)法如下:
JavaScript 代碼:
1. // add.js
2. module.exports = function add(a, b){
3. return a+b;
4. }
上述代碼定義和輸出了一個(gè)模塊。代碼保存在 add.js 文件中。
要使用或?qū)?add 模塊,您需要 require 函數(shù),使用文件名或模塊名作為參數(shù)。下面的語(yǔ)法描述了如何將一個(gè)模塊導(dǎo)入到代碼中:
JavaScript 代碼:
1. var add = require('./add');
如果您在 NodeJS 上編寫(xiě)了代碼,那么這種語(yǔ)法可能看起來(lái)很熟悉。這是因?yàn)?NodeJS 實(shí)現(xiàn)了 CommonJS 風(fēng)格的模塊API。
異步模塊定義(AMD)
CommonJs 風(fēng)格的模塊定義的問(wèn)題在于它是同步的。當(dāng)你調(diào)用 var add=require('add'); 時(shí),系統(tǒng)將暫停,直到模塊 準(zhǔn)備(ready) 完成。這意味著當(dāng)所有的模塊都加載時(shí),這一行代碼將凍結(jié)瀏覽器(愚人碼頭注:意思為除了加載該文件,瀏覽器什么事情也不做)。因此,這可能不是為瀏覽器端應(yīng)用程序定義模塊的最佳方式。
為了把服務(wù)器端用的模塊語(yǔ)法轉(zhuǎn)換給瀏覽器使用,CommonJS 提出了幾種模塊格式,“Module/Transfer” 。其中之一,即 “Module/Transfer/C“,后來(lái)成為 異步模塊定義(AMD) 。
AMD具有以下格式:
JavaScript 代碼:
1. define([‘a(chǎn)dd’, ‘reduce’], function(add, reduce){
2. return function(){...};
3. });
define 函數(shù)(或關(guān)鍵字)將依賴項(xiàng)列表和回調(diào)函數(shù)作為參數(shù)?;卣{(diào)函數(shù)的參數(shù)與數(shù)組中的依賴是相同的順序。這相當(dāng)于導(dǎo)入模塊。并且回調(diào)函數(shù)返回一個(gè)值,即是你導(dǎo)出的值。
CommonJS 和 AMD 解決了模塊模式中剩下的兩個(gè)問(wèn)題:依賴解析 和 全局作用域污染 。我們只需要處理每個(gè)模塊或每個(gè)文件的依賴關(guān)系就可以了。
并且不再有全局作用域污染。
RequireJS
在我們的瀏覽器應(yīng)用程序中,AMD 可以把我們從 script 標(biāo)簽和全局污染中解救出來(lái)。那么,我們?cè)撊绾问褂盟兀窟@里 RequireJS 就可以幫助我們了。RequireJS 是一個(gè) JavaScript 模塊加載器(module loader) 。它可以根據(jù)需要異步加載模塊。
盡管 RequireJS 的名字中含有 require,但是它的目標(biāo)卻并非要去支持 CommonJS 的 require 語(yǔ)法。使用 RequireJS,您可以編寫(xiě) AMD 風(fēng)格的模塊。
在編寫(xiě)自己的應(yīng)用程序之前,你將不得不從 RequireJS 網(wǎng)站 下載 require.js 文件。如下代碼是用 RequireJS 編寫(xiě)的示例應(yīng)用程序。
下面是 AMD 風(fēng)格的應(yīng)用程序示例
HTML 代碼:
1. <!-- 0-index.html -->
2. <!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
14. <script data-main="main" src="require.js"></script>
15. </body>
16. </html>
JavaScript 代碼:
1. // main.js
2. define(['sum'], function(sum){
3. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
4. var answer = sum(values)
5. document.getElementById("answer").innerHTML = answer;
6. })
JavaScript 代碼:
1. // sum.js
2. define(['add', 'reduce'], function(add, reduce){
3. var sum = function(arr){
4. return reduce(arr, add);
5. };
7. return sum;
8. })
JavaScript 代碼:
1. // add.js
2. define([], function(){
3. var add = function(a, b){
4. return a + b;
5. };
7. return add;
8. });
JavaScript 代碼:
1. // reduce.js
2. define([], function(){
3. var reduce = function(arr, iteratee) {
4. var index = 0,
5. length = arr.length,
6. memo = arr[index];
8. index += 1;
9. for(; index < length; index += 1){
10. memo = iteratee(memo, arr[index])
11. }
12. return memo;
13. }
15. return reduce;
16. })
注意在 index.html 中只有一個(gè) script 標(biāo)簽。
HTML 代碼:
1. <script data-main=”main” src=”require.js”></script>
這個(gè)標(biāo)簽加載 require.js 庫(kù)到頁(yè)面,data-main 屬性告訴 RequieJS 應(yīng)用程序的入口點(diǎn)在哪里。默認(rèn)情況下,它假定所有文件都有 .js 文件擴(kuò)展名,所以省略 .js 文件擴(kuò)展名是可以的。在 RequireJS 加載了 main.js 文件之后,就會(huì)加載該文件的依賴,以及依賴的依賴,等等。瀏覽器的開(kāi)發(fā)者工具會(huì)顯示所有文件以如下順序加載(如圖):

瀏覽器加載 index.html,而 index.html 又加載 require.js 。剩下的文件及其依賴都是由 require.js 負(fù)責(zé)加載。
RequireJS 和 AMD 解決了我們以前所遇到的所有問(wèn)題。然而,它也帶來(lái)了一些不那么嚴(yán)重的問(wèn)題。
- AMD 的語(yǔ)法過(guò)于冗余。因?yàn)樗袞|西都封裝在
define函數(shù)中,所以我們的代碼有一些額外的縮進(jìn)。對(duì)于一個(gè)小文件來(lái)說(shuō),這不是什么大問(wèn)題,但是對(duì)于一個(gè)大型的代碼庫(kù)來(lái)說(shuō),這可能是一種精神上的負(fù)擔(dān)。 - 數(shù)組中的依賴列表必須與函數(shù)的參數(shù)列表匹配。如果存在許多依賴項(xiàng),則很難維護(hù)依賴項(xiàng)的順序。如果您的模塊中有幾十個(gè)依賴項(xiàng),并且如果你不得不在中間刪除某個(gè)依賴,那么就很難找到匹配的模塊和參數(shù)。
- 在當(dāng)前瀏覽器下(HTTP 1.1),加載很多小文件會(huì)降低性能。
Browserify
由于上述這些原因,有些人想要使用 CommonJS 語(yǔ)法來(lái)替換。但 CommonJS 語(yǔ)法是用于服務(wù)端,并且是同步的,對(duì)嗎?這時(shí) Browserify 就來(lái)解救我們了!通過(guò) Browserify ,你可以在瀏覽器應(yīng)用程序中使用 CommonJS 模塊。Browserify 是一個(gè) 模塊打包器(module bundler) 。Browserify 遍歷代碼的依賴樹(shù),并將依賴樹(shù)中的所有模塊打包成一個(gè)文件。
不同于 RequireJS ,但是 Browserify 是一個(gè)命令行工具,需要 NodeJS 和 NPM 來(lái)安裝它。如果系統(tǒng)中安裝了 NodeJS ,就可以用如下命令來(lái)安裝 Browserify:
CommandLine 代碼:
1. npm install -g browserify
讓我們看一下我們用 CommonJS 語(yǔ)法編寫(xiě)的示例應(yīng)用程序。
HTML 代碼:
1. <!-- 0-index.html -->
2. <!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
14. <script src="bundle.js"></script>
15. </body>
16. </html>
JavaScript 代碼:
1. //main.js
2. var sum = require('./sum');
3. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
4. var answer = sum(values)
6. document.getElementById("answer").innerHTML = answer;
`
JavaScript 代碼:
1. //sum.js
2. var reduce = require('./reduce');
3. var add = require('./add');
5. module.exports = function(arr){
6. return reduce(arr, add);
7. };
JavaScript 代碼:
1. //add.js
2. module.exports = function add(a,b){
3. return a + b;
4. };
JavaScript 代碼:
1. //reduce.js
2. module.exports = function reduce(arr, iteratee) {
3. var index = 0,
4. length = arr.length,
5. memo = arr[index];
7. index += 1;
8. for(; index < length; index += 1){
9. memo = iteratee(memo, arr[index])
10. }
11. return memo;
12. };
你可能已經(jīng)注意到,在 index.html 文件中,script 標(biāo)記加載了 bundle.js,但是 bundle.js文件在哪里?一旦我們執(zhí)行了如下命令,Browserify 就會(huì)為我們生成這個(gè)文件:
CommandLine 代碼:
1. $ browserify main.js -o bundle.js
Browserify 解析 main.js 中的 require() 函數(shù)調(diào)用,并遍歷項(xiàng)目中的依賴樹(shù)。然后將依賴樹(shù)打包到一個(gè)文件中。
Browserify 生成如下 bundle.js 文件的代碼:
JavaScript 代碼:
1. function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
2. module.exports = function add(a,b){
3. return a + b;
4. };
6. },{}],2:[function(require,module,exports){
7. var sum = require('./sum');
8. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
9. var answer = sum(values)
11. document.getElementById("answer").innerHTML = answer;
13. },{"./sum":4}],3:[function(require,module,exports){
14. module.exports = function reduce(arr, iteratee) {
15. var index = 0,
16. length = arr.length,
17. memo = arr[index];
19. index += 1;
20. for(; index < length; index += 1){
21. memo = iteratee(memo, arr[index])
22. }
23. return memo;
24. };
26. },{}],4:[function(require,module,exports){
27. var reduce = require('./reduce');
28. var add = require('./add');
30. module.exports = function(arr){
31. return reduce(arr, add);
32. };
34. },{"./add":1,"./reduce":3}]},{},[2]);
您不需要逐行地理解這個(gè)打包文件。但值得注意的是,所有熟悉的代碼、main.js 文件和所有依賴項(xiàng)都包含在這個(gè)文件中。
UMD – 只是為了讓你更困惑
現(xiàn)在我們已經(jīng)學(xué)習(xí)了 全局對(duì)象(Global Object),CommonJS 和 AMD 風(fēng)格的模塊。也有一些庫(kù)可以幫助我們直接使用 CommonJS 或者 AMD 。但是,如果您正在編寫(xiě)一個(gè)模塊,并部署到互聯(lián)網(wǎng)上,該怎么辦呢?你需要編寫(xiě)哪種模塊風(fēng)格呢?
編寫(xiě)三種不同的模塊類型,即 全局模塊對(duì)象 、CommonJS 和 AMD 。但是你必須維護(hù)三個(gè)不同的文件。用戶將不得不識(shí)別他們正在下載的模塊的類型。
通用模塊定義(Universal Module Definition) ,即我們通常說(shuō)的 UMD ,就是用來(lái)解決這個(gè)特殊問(wèn)題的。本質(zhì)上,UMD 是一套用來(lái)識(shí)別當(dāng)前環(huán)境支持的模塊風(fēng)格的 if/else 語(yǔ)句。
JavaScript 代碼:
1. // UMD 風(fēng)格編寫(xiě)的 sum 模塊
2. //sum.umd.js
3. (function (root, factory) {
4. if (typeof define === 'function' && define.amd) {
5. // AMD
6. define(['add', 'reduce'], factory);
7. } else if (typeof exports === 'object') {
8. // Node, CommonJS-like
9. module.exports = factory(require('add'), require('reduce'));
10. } else {
11. // Browser globals (root is window)
12. root.sum = factory(root.add, root.reduce);
13. }
14. }(this, function (add, reduce) {
15. // private methods
17. // exposed public methods
18. return function(arr) {
19. return reduce(arr, add);
20. }
21. }));
ES6 模塊語(yǔ)法
愚人碼頭注:了解更多關(guān)于 ES6 模塊的信息,建議閱讀 ECMAScript 6 Modules(模塊)系統(tǒng)及語(yǔ)法詳解。
JavaScript 全局模塊對(duì)象、CommonJS、AMD 和 UMD,我們有太多的選項(xiàng)了?,F(xiàn)在或許你會(huì)問(wèn),下一個(gè)項(xiàng)目我該用哪一個(gè)呢?答案是一個(gè)都不用。
JavaScript 語(yǔ)言中并沒(méi)有內(nèi)置模塊系統(tǒng)。這就是為什么我們有這么多不同的導(dǎo)入和導(dǎo)出模塊的原因。但這種情況最近發(fā)生了變化。 ES6 語(yǔ)言規(guī)范中,模塊是 JavaScript 的一部分。所以這個(gè)問(wèn)題的答案是,如果你想讓你的項(xiàng)目想兼容未來(lái),你需要使用 ES6 模塊語(yǔ)法。
ES6 用 import 和 export 關(guān)鍵字來(lái)導(dǎo)入和導(dǎo)出模塊。如下是用 ES6 模塊語(yǔ)法編寫(xiě)的示例應(yīng)用程序。
JavaScript 代碼:
1. // main.js
2. import sum from "./sum";
4. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
5. var answer = sum(values);
7. document.getElementById("answer").innerHTML = answer;
JavaScript 代碼:
1. // sum.js
2. import add from './add';
3. import reduce from './reduce';
5. export default function sum(arr){
6. return reduce(arr, add);
7. }
JavaScript 代碼:
1. // add.js
2. export default function add(a,b){
3. return a + b;
4. }
JavaScript 代碼:
1. //reduce.js
2. export default function reduce(arr, iteratee) {
3. let index = 0,
4. length = arr.length,
5. memo = arr[index];
7. index += 1;
8. for(; index < length; index += 1){
9. memo = iteratee(memo, arr[index]);
10. }
11. return memo;
12. }
有很多關(guān)于 ES6 模塊的流行語(yǔ):ES6 模塊語(yǔ)法是簡(jiǎn)潔的。ES6 模塊將統(tǒng)治 JavaScript 世界。它是未來(lái)。但不幸的是,有一個(gè)問(wèn)題。瀏覽器還沒(méi)有為這種新語(yǔ)法做好準(zhǔn)備。在撰寫(xiě)文章的時(shí)候,只有 Chrome 瀏覽器支持 import 語(yǔ)句。即使大多數(shù)瀏覽器支持 import 和 export ,如果您的應(yīng)用程序必須支持較老的瀏覽器,那么您可能會(huì)遇到問(wèn)題。
幸運(yùn)的是,現(xiàn)在已經(jīng)有很多工具可以用了,這些工具讓我們現(xiàn)在就可以用 ES6 模塊語(yǔ)法。
Webpack
Webpack 是一個(gè) 模塊打包器(module bundler) 。就像 Browserify 一樣,它會(huì)遍歷依賴樹(shù),然后將其打包到一到多個(gè)文件。那么問(wèn)題來(lái)了,如果它和 Browserify 一樣,為什么我們需要另一個(gè)模塊打包器呢?Webpack 可以處理 CommonJS 、 AMD 和 ES6 模塊。并且 Webpack 還有更多的靈活性和一些很酷的功能特性,比如:
- 代碼分離:當(dāng)您有多個(gè)應(yīng)用程序共享相同的模塊時(shí)。Webpack 可以將您的代碼打包到兩個(gè)或更多的文件中。例如,如果您有兩個(gè)應(yīng)用程序 app1 和 app2 ,并且都共享許多模塊。 使用 Browserify ,你會(huì)有 app1.js 和 app2.js 。并且都包含所有依賴關(guān)系模塊。但是使用 Webpack ,您可以創(chuàng)建 app1.js ,app2.js 和 shared-lib.js。是的,您必須從 html 頁(yè)面加載 2 個(gè)文件。但是使用哈希文件名,瀏覽器緩存和 CDN ,可以減少初始加載時(shí)間。
-
加載器:用自定義加載器,可以加載任何文件到源文件中。用
require()語(yǔ)法,不僅僅可以加載 JavaScript 文件,還可以加載 CSS、CoffeeScript、Sass、Less、HTML模板、圖像,等等。 - 插件:Webpack 插件可以在打包寫(xiě)入到打包文件之前對(duì)其進(jìn)行操作。有很多社區(qū)創(chuàng)建的插件。例如,給打包代碼添加注釋,添加 Source map,將打包文件分離成塊等等。
WebpackDevServer 是一個(gè)開(kāi)發(fā)服務(wù)器,它可以在源代碼改變被檢測(cè)到時(shí)自動(dòng)打包源代碼,并刷新瀏覽器。它通過(guò)提供代碼的即時(shí)反饋,從而加快開(kāi)發(fā)過(guò)程。
讓我們來(lái)看看我們?nèi)绾斡?Webpack 來(lái)構(gòu)建示例應(yīng)用程序。Webpack 需要一點(diǎn)引導(dǎo)和配置。
因?yàn)?Webpack 是 JavaScript 命令行工具,所以需要先安裝上 NodeJS 和 NPM 。裝好 NPM 后,執(zhí)行如下命令初始化項(xiàng)目:
CommandLine 代碼:
1. $ mkdir project; cd project
2. $ npm init -y
3. $ npm install -D webpack webpack-dev-server
您需要一個(gè) webpack 的配置文件。你的配置中至少需要 entry 和 output 兩個(gè)字段。在 webpack.config.js 中保存以下內(nèi)容。
JavaScript 代碼:
1. // webpack.config.js webpack 的配置文件
2. module.exports = {
3. entry: ‘./app/main.js’,
4. output: {
5. filename: ‘bundle.js’
6. }
7. }
打開(kāi) package.json 文件,在 script 字段后添加如下行:
JavaScript 代碼:
1. "scripts": {
2. "start": "webpack-dev-server -progress -colors",
3. "build": "webpack"
4. },
現(xiàn)在在 project/app 目錄下添加所有 JavaScript 模塊,在 project 目錄下添加 index.html。
HTML 代碼:
1. <!-- 0-index.html -->
2. <!DOCTYPE html>
3. <html>
4. <head>
5. <meta charset="UTF-8">
6. <title>JS Modules</title>
7. </head>
8. <body>
9. <h1>
10. The Answer is
11. <span id="answer"></span>
12. </h1>
14. <script src="bundle.js"></script>
15. </body>
16. </html>
JavaScript 代碼:
1. // 02-webpack.config.js
2. module.exports = {
3. entry: './app/main.js',
4. output: {
5. path: './dist',
6. filename: 'bundle.js'
7. }
8. }
JavaScript 代碼:
1. // 03-package.json 特別注意,這行注釋不要復(fù)制,否則json文件會(huì)報(bào)錯(cuò)
2. {
3. "name": "jsmodules",
4. "version": "1.0.0",
5. "description": "",
6. "main": "main.js",
7. "scripts": {
8. "start": "webpack-dev-server --progress --colors",
9. "build": "webpack"
10. },
11. "keywords": [],
12. "author": "",
13. "license": "ISC",
14. "devDependencies": {
15. "webpack": "^1.12.14",
16. "webpack-dev-server": "^1.14.1"
17. }
18. }
JavaScript 代碼:
1. // app/add.js
2. module.exports = function add(a,b){
3. return a + b;
4. };
JavaScript 代碼:
1. // app/reduce.js
2. module.exports = function reduce(arr, iteratee) {
3. var index = 0,
4. length = arr.length,
5. memo = arr[index];
7. index += 1;
8. for(; index < length; index += 1){
9. memo = iteratee(memo, arr[index])
10. }
11. return memo;
12. };
JavaScript 代碼:
1. // app/sum.js
2. define(['./reduce', './add'], function(reduce, add){
3. sum = function(arr){
4. return reduce(arr, add);
5. }
7. return sum;
8. });
JavaScript 代碼:
1. // app/main.js
2. var sum = require('./sum');
3. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
4. var answer = sum(values)
6. document.getElementById("answer").innerHTML = answer;
注意 add.js 和 reduce.js 是用 CommonJS 風(fēng)格寫(xiě)的,而 sum.js 是用 AMD 風(fēng)格寫(xiě)的。 Webpack 默認(rèn)是可以處理 CommonJS 和 AMD。如果你用的是 ES6 模塊,那就需要安裝和配置 babel loader。
一旦你準(zhǔn)備好所有的文件,你可以運(yùn)行你的應(yīng)用程序。
CommandLine 代碼:
1. $ npm start
打開(kāi)瀏覽器,把 URL 指向 http://localhost:8080/webpack-dev-server/,如圖:

此時(shí),你可以打開(kāi)你喜歡的編輯器編輯代碼。保存文件時(shí),瀏覽器會(huì)自動(dòng)刷新以顯示修改后的結(jié)果。
這里你可能會(huì)注意到一件事情,就是找不到 dist/bundle.js 文件。這是因?yàn)?Webpack Dev Server 會(huì)創(chuàng)建打包文件,但是不會(huì)寫(xiě)入到文件系統(tǒng)中,而是放在內(nèi)存中。
如果要部署,就得創(chuàng)建打包文件??梢酝ㄟ^(guò)鍵入如下命令創(chuàng)建 bundle.js 文件:
CommandLine 代碼:
- $ npm run build
如果有興趣學(xué)習(xí)更多的 Webpack 知識(shí),請(qǐng)參考 Webpack 文檔頁(yè) 。
Rollup (2015 年 5 月)
愚人碼頭注:
Rollup 普及了 JavaScript 圈內(nèi)一個(gè)重要的特性:Tree shaking,即是指消除JavaScript上下文中無(wú)用代碼,或更精確地說(shuō),只保留有用的代碼。它依賴于ES6模塊 import / export 模塊系統(tǒng)的靜態(tài)結(jié)構(gòu)(static structure)來(lái)檢測(cè)哪一個(gè)模塊沒(méi)有被使用,因?yàn)椋琲mport 和 export 不會(huì)在運(yùn)行時(shí)改變。說(shuō)的再直白一點(diǎn)就是 Tree shaking 從模塊包中排除未使用的 exports 項(xiàng)。
webpack 2 內(nèi)置引入的 Tree-shaking 代碼優(yōu)化技術(shù)。 詳情閱讀 webpack 2 中的 Tree Shaking
將一個(gè)大的 JavaScript 庫(kù)包含進(jìn)來(lái),只是為了用它幾個(gè)函數(shù),你是否有這樣的經(jīng)歷?Rollup 是另一個(gè) JavaScript ES6 模塊打包器。與 Browserify 和 Webpak 不同,rollup 只包含在項(xiàng)目中用到的代碼。如果有大模塊,帶有很多函數(shù),但是你只是用到少數(shù)幾個(gè),rollup 只會(huì)將需要的函數(shù)包含到打包文件中,從而顯著減少打包文件大小。
Rollup 可以被用作為命令行工具。如果有 NodeJS 和 NPM,那么就可以用如下命令安裝 rollup:
CommandLine 代碼:
1. $ npm install -g rollup
Rollup 可以與任何類型的模塊風(fēng)格一起工作。但是,推薦使用 ES6 模塊風(fēng)格,這樣就可以利用 tree-shaking 功能。如下是用 ES6 編寫(xiě)的示例應(yīng)用程序代碼:
JavaScript 代碼:
1. // 01-add.js
2. let add = (a,b) => a + b;
3. let sub = (a,b) => a - b;
5. export { add, sub };
JavaScript 代碼:
1. // reduce.js
2. export default (arr, iteratee) => {
3. let index = 0,
4. length = arr.length,
5. memo = arr[index];
7. index += 1;
8. for(; index < length; index += 1){
9. memo = iteratee(memo, arr[index]);
10. }
11. return memo;
12. }
JavaScript 代碼:
1. // sum.js
2. import { add } from './add';
3. import reduce from './reduce';
5. export default (arr) => reduce(arr, add);
JavaScript 代碼:
1. // main.js
2. import sum from "./sum";
4. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
5. var answer = sum(values);
7. document.getElementById("answer").innerHTML = answer;
注意,在 add 模塊中,我引入了另一個(gè)函數(shù) sub()。但是該函數(shù)在應(yīng)用程序中并沒(méi)有用到。
現(xiàn)在我們用 rollup 將這些代碼打包:
CommandLine 代碼:
1. $ rollup main.js -o bundle.js
這會(huì)生成像如下的 bundle.js 文件:
JavaScript 代碼:
1. // bundle.js
2. let add = (a,b) => a + b;
4. var reduce = (arr, iteratee) => {
5. let index = 0,
6. length = arr.length,
7. memo = arr[index];
9. index += 1;
10. for(; index < length; index += 1){
11. memo = iteratee(memo, arr[index]);
12. }
13. return memo;
14. }
16. var sum = (arr) => reduce(arr, add);
18. var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ];
19. var answer = sum(values);
21. document.getElementById("answer").innerHTML = answer;
這里我們可以看到 sub() 函數(shù)并沒(méi)有包含在這個(gè)打包文件中。
SystemJS
SystemJS 是一個(gè)通用的模塊加載器,它能在瀏覽器或者 NodeJS 上動(dòng)態(tài)加載模塊,并且支持 CommonJS、AMD、全局模塊對(duì)象和 ES6 模塊。通過(guò)使用插件,它不僅可以加載 JavaScript,還可以加載 CoffeeScript 和 TypeScript。
SystemJS 的另一個(gè)優(yōu)點(diǎn)是,它建立在 ES6 模塊加載器之上,所以它的語(yǔ)法和 API 在將來(lái)很可能是語(yǔ)言的一部分,這會(huì)讓我們的代碼更不會(huì)過(guò)時(shí)。
要異步輸入一個(gè)模塊,可以用如下語(yǔ)法:
JavaScript 代碼:
1. System.import(‘module-name’);
然后我們可以用配置 API 來(lái)配置 SystemJS 的行為:
JavaScript 代碼:
1. System.config({
2. transplier: ‘babel’,
3. baseURL: ‘/app’
4. });
上面的配置會(huì)讓 SystemJS 使用 babel 作為 ES6 模塊的編譯器,并且從 /app 目錄加載模塊。
隨著現(xiàn)代 JavaScript 應(yīng)用程序變得越來(lái)越大,越來(lái)越復(fù)雜,開(kāi)發(fā)工作流也是如此。所以我們不僅僅模塊加載器,還得去尋找開(kāi)發(fā)服務(wù)器、生產(chǎn)的模塊打包器以及第三方模塊的包管理器。
JSPM
JSPM 是 JavaScript 開(kāi)發(fā)工具的瑞士軍刀,它是既是包管理器,又是模塊加載器,又是模塊打包器。
現(xiàn)代 JavaScript 開(kāi)發(fā)很少只是需要自己的模塊,絕大部分時(shí)候,我們還需要第三方模塊。使用 JSPM,我們可以使用如下的命令,從 NPM 或者 Github 安裝第三方模塊:
CommandLine 代碼:
1. jspm install npm:package-name or github:package/name
上述命令會(huì)從 npm 或者 github 下載包,并將包安裝到 jspm_packages 目錄。
在開(kāi)發(fā)模式下,我們可以使用 jspm-server 。像 Webpack Dev Server 一樣,它會(huì)檢測(cè)代碼改變,重新加載瀏覽器來(lái)顯示改變。與 Webpack Dev Server 不同的是,jspm-server 用的是 SystemJS 模塊加載器。所以,每次它檢測(cè)了文件的改變時(shí),不會(huì)將所有文件讀取來(lái)打包,而是只加載頁(yè)面所需要的模塊。
在部署時(shí),肯定要打包代碼。JSPM 帶有打包器,可以用如下命令對(duì)代碼打包:
CommandLine 代碼:
1. jspm bundle main.js bundle.js
在幕后,JSPM 用 rollup 作為它的打包器。
總結(jié)
我希望本文給了足夠的信息來(lái)理解 JavaScript 模塊的詞匯?,F(xiàn)在你也許會(huì)問(wèn),下一個(gè)項(xiàng)目我應(yīng)該用什么呢?不幸的是,我回答不了這個(gè)問(wèn)題。現(xiàn)在你有能力開(kāi)始自己的探索。希望本文能讓你更容易理解我提到的有關(guān)工具的文檔和文章。