JavaScript 是如何工做的:模塊的構建以及對應的打包工具

阿里雲最近在作活動,低至2折,有興趣能夠看看:
https://promotion.aliyun.com/...

爲了保證的可讀性,本文采用意譯而非直譯。javascript

這是專門探索 JavaScript 及其所構建的組件的系列文章的第 20 篇。html

若是你錯過了前面的章節,能夠在這裏找到它們:前端

  1. JavaScript 是如何工做的:引擎,運行時和調用堆棧的概述!
  2. JavaScript 是如何工做的:深刻V8引擎&編寫優化代碼的5個技巧!
  3. JavaScript 是如何工做的:內存管理+如何處理4個常見的內存泄漏!
  4. JavaScript 是如何工做的:事件循環和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
  5. JavaScript 是如何工做的:深刻探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
  6. JavaScript 是如何工做的:與 WebAssembly比較 及其使用場景!
  7. JavaScript 是如何工做的:Web Workers的構建塊+ 5個使用他們的場景!
  8. JavaScript 是如何工做的:Service Worker 的生命週期及使用場景!
  9. JavaScript 是如何工做的:Web 推送通知的機制!
  10. JavaScript 是如何工做的:使用 MutationObserver 跟蹤 DOM 的變化!
  11. JavaScript 是如何工做的:渲染引擎和優化其性能的技巧!
  12. JavaScript 是如何工做的:深刻網絡層 + 如何優化性能和安全!
  13. JavaScript 是如何工做的:CSS 和 JS 動畫底層原理及如何優化它們的性能!
  14. JavaScript 是如何工做的:解析、抽象語法樹(AST)+ 提高編譯速度5個技巧!
  15. JavaScript 是如何工做的:深刻類和繼承內部原理+Babel和 TypeScript 之間轉換!
  16. JavaScript 是如何工做的:存儲引擎+如何選擇合適的存儲API!
  17. JavaScript 是如何工做的:Shadow DOM 的內部結構+如何編寫獨立的組件!
  18. JavaScript 是如何工做的:WebRTC 和對等網絡的機制!
  19. JavaScript 是如何工做的:編寫本身的 Web 開發框架 + React 及其虛擬 DOM 原理!

圖片描述

若是你是 JavaScript 的新手,一些像 「module bundlers vs module loaders」、「Webpack vs Browserify」 和 「AMD vs.CommonJS」 這樣的術語,很快讓你不堪重負。java

JavaScript 模塊系統可能使人生畏,但理解它對 Web 開發人員相當重要。jquery

在這篇文章中,我將以簡單的言語(以及一些代碼示例)爲你解釋這些術語。 但願這對你有會有幫助!webpack

什麼是模塊?

好做者能將他們的書分紅章節,優秀的程序員將他們的程序劃分爲模塊。git

就像書中的章節同樣,模塊只是文字片斷(或代碼,視狀況而定)的集羣。然而,好的模塊是高內聚低鬆耦的,具備不一樣的功能,容許在必要時對它們進行替換、刪除或添加,而不會擾亂總體功能。程序員

爲何使用模塊?

使用模塊有利於擴展、相互依賴的代碼庫,這有不少好處。在我看來,最重要的是:es6

1)可維護性: 根據定義,模塊是高內聚的。一個設計良好的模塊旨在儘量減小對代碼庫部分的依賴,這樣它就能夠獨立地加強和改進,當模塊與其餘代碼片斷解耦時,更新單個模塊要容易得多。github

回到咱們的書的例子,若是你想要更新你書中的一個章節,若是對一個章節的小改動須要你調整每個章節,那將是一場噩夢。相反,你但願以這樣一種方式編寫每一章,便可以在不影響其餘章節的狀況下進行改進。

2)命名空間: 在 JavaScript 中,頂級函數範圍以外的變量是全局的(這意味着每一個人均可以訪問它們)。所以,「名稱空間污染」很常見,徹底不相關的代碼共享全局變量。

在不相關的代碼之間共享全局變量在開發中是一個大禁忌。正如咱們將在本文後面看到的,經過爲變量建立私有空間,模塊容許咱們避免名稱空間污染。

3)可重用性:坦白地說:咱們將前寫過的代碼複製到新項目中。 例如,假設你從以前項目編寫的一些實用程序方法複製到當前項目中。

這一切都很好,但若是你找到一個更好的方法來編寫代碼的某些部分,那麼你必須記得回去在曾經使用過的其餘項目更新它。

這顯然是在浪費時間。若是有一個咱們能夠一遍又一遍地重複使用的模塊,不是更容易嗎?

如何建立模塊?

有多種方法來建立模塊,來看幾個:

模塊模式

模塊模式用於模擬類的概念(由於 JavaScript 自己不支持類),所以咱們能夠在單個對象中存儲公共和私有方法和變量——相似於在 Java 或 Python 等其餘編程語言中使用類的方式。這容許咱們爲想要公開的方法建立一個面向公共的 API,同時仍然將私有變量和方法封裝在閉包範圍中。

有幾種方法能夠實現模塊模式。在第一個示例中,將使用匿名閉包,將全部代碼放在匿名函數中來幫助咱們實現目標。(記住:在 JavaScript 中,函數是建立新做用域的惟一方法。)

例一:匿名閉包

(function () {
  // 將這些變量放在閉包範圍內實現私有化
  
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item}, 0);
    
      return '平均分 ' + total / myGrades.length + '.';
  }

  var failing = function(){
    var failingGrades = myGrades.filter(function(item) {
      return item < 70;});
      
    return '掛機科了 ' + failingGrades.length + ' 次。';
  }

  console.log(failing()); // 掛機科了次

}());

使用這個結構,匿名函數就有了本身的執行環境或「閉包」,而後咱們當即執行。這讓咱們能夠從父(全局)命名空間隱藏變量。

這種方法的優勢是,你能夠在這個函數中使用局部變量,而不會意外地覆蓋現有的全局變量,但仍然能夠訪問全局變量,就像這樣:

var global = '你好,我是一個全局變量。)';
    
   (function () {
      // 將這些變量放在閉包範圍內實現私有化
      
      var myGrades = [93, 95, 88, 0, 55, 91];
      
      var average = function() {
        var total = myGrades.reduce(function(accumulator, item) {
          return accumulator + item}, 0);
        
          return '平均分 ' + total / myGrades.length + '.';
      }
    
      var failing = function(){
        var failingGrades = myGrades.filter(function(item) {
          return item < 70;});
          
        return '掛機科了 ' + failingGrades.length + ' 次。';
      }
    
      console.log(failing()); // 掛機科了次
      onsole.log(global); // 你好,我是一個全局變量。
    
    }());

注意,匿名函數的圓括號是必需的,由於以關鍵字 function 開頭的語句一般被認爲是函數聲明(請記住,JavaScript 中不能使用未命名的函數聲明)。所以,周圍的括號將建立一個函數表達式,並當即執行這個函數,這還有另外一種叫法 當即執行函數(IIFE)。若是你對這感興趣,能夠在這裏瞭解到更多。

例二:全局導入

jQuery 等庫使用的另外一種流行方法是全局導入。它相似於咱們剛纔看到的匿名閉包,只是如今咱們做爲參數傳入全局變量:

(function (globalVariable) {

  // 在這個閉包範圍內保持變量的私有化
  var privateFunction = function() {
    console.log('Shhhh, this is private!');
  }

  // 經過 globalVariable 接口公開下面的方法
 // 同時將方法的實現隱藏在 function() 塊中

  globalVariable.each = function(collection, iterator) {
    if (Array.isArray(collection)) {
      for (var i = 0; i < collection.length; i++) {
        iterator(collection[i], i, collection);
      }
    } else {
      for (var key in collection) {
        iterator(collection[key], key, collection);
      }
    }
  };

  globalVariable.filter = function(collection, test) {
    var filtered = [];
    globalVariable.each(collection, function(item) {
      if (test(item)) {
        filtered.push(item);
      }
    });
    return filtered;
  };

  globalVariable.map = function(collection, iterator) {
    var mapped = [];
    globalUtils.each(collection, function(value, key, collection) {
      mapped.push(iterator(value));
    });
    return mapped;
  };

  globalVariable.reduce = function(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    globalVariable.each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;

  };

 }(globalVariable));

在這個例子中,globalVariable 是惟一的全局變量。與匿名閉包相比,這種方法的好處是能夠預先聲明全局變量,使得別人更容易閱讀代碼。

例三:對象接口

另外一種方法是使用當即執行函數接口對象建立模塊,以下所示:

var myGradesCalculate = (function () {
    
  // 將這些變量放在閉包範圍內實現私有化
  var myGrades = [93, 95, 88, 0, 55, 91];

  // 經過接口公開這些函數,同時將模塊的實現隱藏在function()塊中

  return {
    average: function() {
      var total = myGrades.reduce(function(accumulator, item) {
        return accumulator + item;
        }, 0);
        
      return'平均分 ' + total / myGrades.length + '.';
    },

    failing: function() {
      var failingGrades = myGrades.filter(function(item) {
          return item < 70;
        });

      return '掛科了' + failingGrades.length + ' 次.';
    }
  }
})();

myGradesCalculate.failing(); // '掛科了 2 次.' 
myGradesCalculate.average(); // '平均分 70.33333333333333.'

正如您所看到的,這種方法容許咱們經過將它們放在 return 語句中(例如算平均分和掛科數方法)來決定咱們想要保留的變量/方法(例如 myGrades)以及咱們想要公開的變量/方法。

例四:顯式模塊模式

這與上面的方法很是類似,只是它確保全部方法和變量在顯式公開以前都是私有的:

var myGradesCalculate = (function () {
    
  // 將這些變量放在閉包範圍內實現私有化
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item;
      }, 0);
      
    return'平均分 ' + total / myGrades.length + '.';
  };

  var failing = function() {
    var failingGrades = myGrades.filter(function(item) {
        return item < 70;
      });

    return '掛科了' + failingGrades.length + ' 次.';
  };

  // Explicitly reveal public pointers to the private functions 
  // that we want to reveal publicly

  return {
    average: average,
    failing: failing
  }
})();

myGradesCalculate.failing(); // '掛科了 2 次.' 
myGradesCalculate.average(); // '平均分 70.33333333333333.'

這可能看起來不少,但它只是模塊模式的冰山一角。 如下是我在本身的探索中發現有用的一些資源:

CommonJS 和 AMD

全部這些方法都有一個共同點:使用單個全局變量將其代碼包裝在函數中,從而使用閉包做用域爲本身建立一個私有名稱空間。

雖然每種方法都有效且都有各自特色,但卻都有缺點。

首先,做爲開發人員,你須要知道加載文件的正確依賴順序。例如,假設你在項目中使用 Backbone,所以你能夠將 Backbone 的源代碼 以<script> 腳本標籤的形式引入到文件中。

可是,因爲 Backbone 對 Underscore.js 有很強的依賴性,所以 Backbone 文件的腳本標記不能放在Underscore.js 文件以前。

做爲一名開發人員,管理依賴關係並正確處理這些事情有時會使人頭痛。

另外一個缺點是它們仍然會致使名稱空間衝突。例如,若是兩個模塊具備相同的名稱怎麼辦?或者,若是有一個模塊的兩個版本,而且二者都須要,該怎麼辦?

幸運的是,答案是確定的。

有兩種流行且實用的方法:CommonJSAMD

CommonJS

CommonJS 是一個志願者工做組,負責設計和實現用於聲明模塊的 JavaScript API。

CommonJS 模塊本質上是一個可重用的 JavaScript,它導出特定的對象,使其可供其程序中須要的其餘模塊使用。 若是你已經使用 Node.js 編程,那麼你應該很是熟悉這種格式。

使用 CommonJS,每一個 JavaScript 文件都將模塊存儲在本身獨立的模塊上下文中(就像將其封裝在閉包中同樣)。 在此範圍內,咱們使用 module.exports 導出模塊,或使用 require 來導入模塊。

在定義 CommonJS 模塊時,它多是這樣的:

function myModule() {
  this.hello = function() {
    return 'hello!';
  }
   
  this.goodbye = function() {
    return 'goodbye!';
  }
}

module.exports = myModule;

咱們使用特殊的對象模塊,並將函數的引用放入 module.exports 中。這讓 CommonJS 模塊系統知道咱們想要公開什麼,以便其餘文件可使用它。

若是想使用 myModule,只須要使用 require 方法就能夠,以下:

var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

與前面討論的模塊模式相比,這種方法有兩個明顯的好處:

  1. 避免全局命名空間污染
  2. 依賴關係更加明確

另外須要注意的是,CommonJS 採用服務器優先方法並同步加載模塊。 這很重要,由於若是咱們須要三個其餘模塊,它將逐個加載它們。

如今,它在服務器上運行良好,但遺憾的是,在爲瀏覽器編寫 JavaScript 時使用起來更加困難。 能夠這麼說,從網上讀取模塊比從磁盤讀取須要更長的時間。 只要加載模塊的腳本正在運行,它就會阻止瀏覽器運行其餘任何內容,直到完成加載,這是由於 JavaScript 是單線程且 CommonJS 是同步加載的。

AMD

CommonJS一切都很好,可是若是咱們想要異步加載模塊呢? 答案是 異步模塊定義,簡稱 AMD

使用 AMD 的加載模塊以下:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

define 函數的第一個參數是一個數組,數組中是依賴的各類模塊。這些依賴模塊在後臺(以非阻塞的方式)加載進來,一旦加載完畢,define 函數就會調用第二個參數,即回調函數執行操做。

接下來,回調函數接收參數,即依賴模塊 - 示例中就是 myModulemyOtherModule - 容許函數使用這些依賴項, 最後,所依賴的模塊自己也必須使用 define 關鍵字來定義。例如,myModule以下所示:

define([], function() {

  return {
    hello: function() {
      console.log('hello');
    },
    goodbye: function() {
      console.log('goodbye');
    }
  };
});

所以,與 CommonJS 不一樣,AMD 採用瀏覽器優先的方法和異步行爲來完成工做。 (注意,有不少人堅信在開始運行代碼時動態加載文件是不利的,咱們將在下一節關於模塊構建的內容中探討更多內容)。

除了異步性,AMD 的另外一個好處是模塊能夠是對象,函數,構造函數,字符串,JSON 和許多其餘類型,而CommonJS 只支持對象做爲模塊。

也就是說,和CommonJS相比,AMD不兼容io、文件系統或者其餘服務器端的功能特性,並且函數包裝語法與簡單的require 語句相比有點冗長。

UMD

對於同時支持 AMD 和 CommonJS 特性的項目,還有另外一種格式:通用模塊定義(Universal Module Definition, UMD)。

UMD 本質上創造了一種使用二者之一的方法,同時也支持全局變量定義。所以,UMD 模塊可以同時在客戶端和服務端同時工做。

簡單看一下 UMD 是怎樣工做的:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
    define(['myModule', 'myOtherModule'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
  } else {
    // Browser globals (Note: root is window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
  }
}(this, function (myModule, myOtherModule) {
  // Methods
  function notHelloOrGoodbye(){}; // A private method
  function hello(){}; // A public method because it's returned (see below)
  function goodbye(){}; // A public method because it's returned (see below)

  // Exposed public methods
  return {
      hello: hello,
      goodbye: goodbye
  }
}));

Github 上 enlightening repo 裏有更多關於 UMD 的例子。

Native JS

你可能已經注意到,上面的模塊都不是 JavaScript 原生的。相反,咱們已經建立了經過使用模塊模式、CommonJS 或 AMD 來模擬模塊系統的方法。

幸運的是,TC39(定義 ECMAScript 的語法和語義的標準組織)一幫聰明的人已經引入了ECMAScript 6(ES6)的內置模塊。

ES6 爲導入導出模塊提供了不少不一樣的可能性,已經有許多其餘人花時間解釋這些,下面是一些有用的資源:

與 CommonJS 或 AMD 相比,ES6 模塊最大的優勢在於它可以同時提供兩方面的優點:簡明的聲明式語法和異步加載,以及對循環依賴項的更好支持。

也許我我的最喜歡的 ES6 模塊功能是它的導入模塊是導出時模塊的實時只讀視圖。(相比起 CommonJS,導入的是導出模塊的拷貝副本,所以也不是實時的)。

下面是一個例子:

// lib/counter.js

var counter = 1;

function increment() {
  counter++;
}

function decrement() {
  counter--;
}

module.exports = {
  counter: counter,
  increment: increment,
  decrement: decrement
};


// src/main.js

var counter = require('../../lib/counter');

counter.increment();
console.log(counter.counter); // 1

在這個例子中,咱們基本上建立了兩個模塊的對象:一個用於導出它,一個在咱們須要的時候引入。

此外,在 main.js 中的對象目前是與原始模塊是相互獨立的,這就是爲何即便咱們執行 increment 方法,它仍然返回 1,由於引入的變量和最初導入的變量是毫無關聯的。須要改變你引入的對象惟一的方式是手動執行增長:

counter.counter++;
console.log(counter.counter); // 2

另外一方面,ES6建立了咱們導入的模塊的實時只讀視圖:

// lib/counter.js
export let counter = 1;

export function increment() {
  counter++;
}

export function decrement() {
  counter--;
}


// src/main.js
import * as counter from '../../counter';

console.log(counter.counter); // 1
counter.increment();

console.log(counter.counter); // 2

超酷?我發現這一點是由於ES6容許你能夠把你定義的模塊拆分紅更小的模塊而不用刪減功能,而後你還能反過來把它們合成到一塊兒, 徹底沒問題。

什麼是模塊打包?

整體上看,模塊打包只是將一組模塊(及其依賴項)以正確的順序拼接到一個文件(或一組文件)中的過程。正如 Web開發的其它方方面面,棘手的問題老是潛藏在具體的細節裏。

爲何須要打包?

將程序劃分爲模塊時,一般會將這些模塊組織到不一樣的文件和文件夾中。 有可能,你還有一組用於正在使用的庫的模塊,如 Underscore 或 React。

所以,每一個文件都必須以一個 <script> 標籤引入到主 HTML 文件中,而後當用戶訪問你的主頁時由瀏覽器加載進來。 每一個文件使用 <script> 標籤引入,意味着瀏覽器不得不分別逐個的加載它們。

這對於頁面加載時間來講簡直是噩夢。

爲了解決這個問題,咱們將全部文件打包或「拼接」到一個大文件(或視狀況而定的幾個文件),以減小請求的數量。 當你聽到開發人員談論「構建步驟」或「構建過程」時,這就是他們所談論的內容。

另外一種加速構建操做的經常使用方法是「縮減」打包代碼。 縮減是從源代碼中移除沒必要要的字符(例如,空格,註釋,換行符等)的過程,以便在不改變代碼功能的狀況下減小內容的總體大小。

較少的數據意味着瀏覽器處理時間會更快,從而減小了下載文件所需的時間。 若是你見過具備 「min」 擴展名的文件,如 「underscore-min.js」 ,可能會注意到與完整版相比,縮小版本很是小(不過很難閱讀)。

除了捆綁和/或加載模塊以外,模塊捆綁器還提供了許多其餘功能,例如在進行更改時生成自動從新編譯代碼或生成用於調試的源映射。

構建工具(如 Gulp 和 Grunt)能爲開發者直接進行拼接和縮減,確保爲開發人員提供可讀代碼,同時有利於瀏覽器執行的代碼。

打包模塊有哪些不一樣的方法?

當你使用一種標準模塊模式(上部分討論過)來定義模塊時,拼接和縮減文件很是有用。 你真正在作的就是將一堆普通的 JavaScript 代碼捆綁在一塊兒。

可是,若是你堅持使用瀏覽器沒法解析的非原生模塊系統(如 CommonJS 或 AMD(甚至是原生 ES6模塊格式)),則須要使用專門工具將模塊轉換爲排列正確、瀏覽器可解析的代碼。 這就是 Browserify,RequireJS,Webpack 和其餘「模塊打包工具」或「模塊加載工具」的用武之地。

除了打包和/或加載模塊以外,模塊打包器還提供了許多其餘功能,例如在進行更改時生成自動從新編譯代碼或生成用於調試的源映射。

下面是一些常見的模塊打包方法:

打包 CommonJS

正如前面所知道的,CommonJS以同步方式加載模塊,這沒有什麼問題,只是它對瀏覽器不實用。我提到過有一個解決方案——其中一個是一個名爲 Browserify 的模塊打包工具。Browserify 是一個爲瀏覽器編譯 CommonJS模塊的工具。

例如,有個 main.js 文件,它導入一個模塊來計算一組數字的平均值:

var myDependency = require(‘myDependency’);

var myGrades = [93, 95, 88, 0, 91];

var myAverageGrade = myDependency.average(myGrades);

在這種狀況下,咱們有一個依賴項(myDependency),使用下面的命令,Browserify 以 main.js 爲入口把全部依賴的模塊遞歸打包成一個文件:

browserify main.js -o bundle.js

Browserify 經過跳入文件分析每個依賴的 抽象語法樹(AST),以便遍歷項目的整個依賴關係圖。一旦肯定了依賴項的結構,就把它們按正確的順序打包到一個文件中。而後,在 html 裏插入一個用於引入 「bundle.js」<script> 標籤,從而確保你的源代碼在一個 HTTP 請求中完成下載。

相似地,若是有多個文件且有多個依賴時,只需告訴 Browserify 的入口文件路徑便可。最後打包後的文件能夠經過 Minify-JS 之類的工具壓縮打包後的代碼。

打包 AMD

若是你正在使用 AMD,你須要使用像 RequireJS 或者 Curl 這樣的 AMD 加載器。模塊加載器(與模塊打包工具不一樣)會動態加載程序須要運行的模塊。

提醒一下,AMD 與 CommonJS 的主要區別之一是它以異步方式加載模塊。 從這個意義上說,對於 AMD,從技術上講,實際上並不須要構建步驟,由於異步加載模塊意味着在運行過程當中逐步下載那些程序所須要的文件,而不是用戶剛進入頁面就一下把全部文件都下載下來。

但實際上,對於每一個用戶操做而言,隨着時間的推移,大容量請求的開銷在生產中沒有多大意義。 大多數 Web 開發人員仍然使用構建工具打包和壓縮 AMD 模塊以得到最佳性能,例如使用 RequireJS 優化器,r.js 等工具。

總的來講,AMD 和 CommonJS 在打包方面的區別在於:在開發期間,AMD 能夠省去任何構建過程。固然,在代碼上線前,要使用優化工具(如 r.js)進行優化。

Webpack

就打包工具而言,Webpack 是一個新事物。它被設計成與你使用的模塊系統無關,容許開發人員在適當的狀況下使用 CommonJS、AMD 或 ES6。

你可能想知道,爲何咱們須要 Webpack,而咱們已經有了其餘打包工具了,好比 Browserify 和 RequireJS,它們能夠完成工做,而且作得很是好。首先,Webpack 提供了一些有用的特性,好比 「代碼分割」(code
splitting) —— 一種將代碼庫分割爲「塊(chunks)」的方式,從而能實現按需加載。

例如,若是你的 Web 應用程序,其中只須要某些代碼,那麼將整個代碼庫都打包進一個大文件就不是很高效。 在這種狀況下,可使用代碼分割,將須要的部分代碼抽離在"打包塊",在執行按需加載,從而避免在最開始就遇到大量負載的麻煩。

代碼分割只是 Webpack 提供的衆多引人注目的特性之一,網上有不少關於 「Webpack 與 Browserify 誰更好」 的激烈討論。如下是一些客觀冷靜的討論,幫助我稍微理清了頭緒:

ES6 模塊

當前 JS 模塊規範(CommonJS, AMD) 與 ES6 模塊之間最重要的區別是 ES6 模塊的設計考慮到了靜態分析。這意味着當你導入模塊時,導入的模塊在編譯階段也就是代碼開始運行以前就被解析了。這容許咱們在運行程序以前移,移除那些在導出模塊中不被其它模塊使用的部分。移除不被使用的模塊能節省空間,且有效地減小瀏覽器的壓力。

一個常見的問題,使用一些工具,如 Uglify.js ,縮減代碼時,有一個死碼刪除的處理,它和 ES6 移除沒用的模塊又有什麼不一樣呢?只能說 「視狀況而定」。

死碼消除(Dead codeelimination)是一種編譯器原理中編譯最優化技術,它的用途是移除對程序運行結果沒有任何影響的代碼。移除這類的代碼有兩種優勢,不但能夠減小程序的大小,還能夠避免程序在運行中進行不相關的運算行爲,減小它運行的時間。不會被運行到的代碼(unreachable code)以及只會影響到無關程序運行結果的變量(Dead Variables),都是死碼(Dead code)的範疇。

有時,在 UglifyJS 和 ES6 模塊之間死碼消除的工做方式徹底相同,有時則否則。若是你想驗證一下, Rollup’s wiki 裏有個很好的示例。

ES6 模塊的不一樣之處在於死碼消除的不一樣方法,稱爲「tree shaking」。「tree shaking」 本質上是死碼消除反過程。它只包含包須要運行的代碼,而非排除不須要的代碼。來看個例子:

假設有一個帶有多個函數的 utils.js 文件,每一個函數都用 ES6 的語法導出:

export function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 }

export function filter(collection, test) {
  var filtered = [];
  each(collection, function(item) {
    if (test(item)) {
      filtered.push(item);
    }
  });
  return filtered;
}

export function map(collection, iterator) {
  var mapped = [];
  each(collection, function(value, key, collection) {
    mapped.push(iterator(value));
  });
  return mapped;
}

export function reduce(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;
}

接着,假設咱們不知道要在程序中使用什麼 utils.js 中的哪一個函數,因此咱們將上述的全部模塊導入main.js中,以下所示:

import * as Utils from ‘./utils.js’;

最終,咱們只用到的 each 方法:

import * as Utils from ‘./utils.js’;

Utils.each([1, 2, 3], function(x) { console.log(x) });

「tree shaken」 版本的 main.js 看起來以下(一旦模塊被加載後):

function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 };

each([1, 2, 3], function(x) { console.log(x) });

注意:只導出咱們使用的 each 函數。

同時,若是決定使用 filte r函數而不是每一個函數,最終會看到以下的結果:

import * as Utils from ‘./utils.js’;

Utils.filter([1, 2, 3], function(x) { return x === 2 });

tree shaken 版本以下:

function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 };

function filter(collection, test) {
  var filtered = [];
  each(collection, function(item) {
    if (test(item)) {
      filtered.push(item);
    }
  });
  return filtered;
};

filter([1, 2, 3], function(x) { return x === 2 });

此時,each 和 filter 函數都被包含進來。這是由於 filter 在定義時使用了 each。所以也須要導出該函數模塊以保證程序正常運行。

構建 ES6 模塊

咱們知道 ES6 模塊的加載方式與其餘模塊格式不一樣,但咱們仍然沒有討論使用 ES6 模塊時的構建步驟。

遺憾的是,由於瀏覽器對 ES6模 塊的原生支持還不夠完善,因此現階段還須要咱們作一些補充工做。

圖片描述

下面是幾個在瀏覽器中 構建/轉換 ES6 模塊的方法,其中第一個是目前最經常使用的方法:

  1. 使用轉換器(例如 Babel 或 Traceur)以 CommonJS、AMD 或 UMD 格式將 ES6 代碼轉換爲 ES5 代碼,而後再經過 Browserify 或 Webpack 一類的構建工具來進行構建。
  2. 使用 Rollup.js,這其實和上面差很少,只是 Rollup 捎帶 ES6 模塊的功能,在打包以前靜態分析ES6 代碼和依賴項。 它利用 「tree shaking」 技術來優化你的代碼。 總言,當您使用ES6模塊時,Rollup.js 相對於 Browserify 或 Webpack 的主要好處是 tree shaking 能讓打包文件更小。 須要注意的是,Rollup提 供了幾種格式來的打包代碼,包括 ES6,CommonJS,AMD,UMD 或 IIFE。 IIFE 和 UMD 捆綁包能夠直接在瀏覽器中工做,但若是你選擇打包 AMD,CommonJS 或 ES6,需須要尋找能將代碼轉成瀏覽器能理解運行的代碼的方法(例如,使用 Browserify, Webpack,RequireJS等)。

當心踩坑

做爲 web 開發人員,咱們必須經歷不少困難。轉換語法優雅的ES6代碼以便在瀏覽器裏運行並不老是容易的。

問題是,何時 ES6 模塊能夠在瀏覽器中運行而不須要這些開銷?

答案是:「儘快」。

ECMAScript 目前有一個解決方案的規範,稱爲 ECMAScript 6 module loader API。簡而言之,這是一個綱領性的、基於 Promise 的 API,它支持動態加載模塊並緩存它們,以便後續導入不會從新加載模塊的新版本。

它看起來以下:

// myModule.js

export class myModule {
  constructor() {
    console.log('Hello, I am a module');
  }

  hello() {
    console.log('hello!');
  }

  goodbye() {
    console.log('goodbye!');
  }
}


// main.js
System.import(‘myModule’).then(function(myModule) {
  new myModule.hello();
});

// ‘hello!’

你亦可直接對 script 標籤指定 「type=module」 來定義模塊,如:

<script type="module">
  // loads the 'myModule' export from 'mymodule.js'
  import { hello } from 'mymodule';

  new Hello(); // 'Hello, I am a module!'
</script>

更加詳細的介紹也能夠在 Github 上查看:es-module-loader

此外,若是您想測試這種方法,請查看 SystemJS,它創建在 ES6 Module Loader polyfill 之上。 SystemJS 在瀏覽器和 Node 中動態加載任何模塊格式(ES6模塊,AMD,CommonJS 或 全局腳本)。

它跟蹤「模塊註冊表」中全部已加載的模塊,以免從新加載先前已加載過的模塊。 更不用說它還會自動轉換ES6模塊(若是隻是設置一個選項)而且可以從任何其餘類型加載任何模塊類型!

有了原生的 ES6 模塊後,還須要模塊打包嗎?

對於日益普及的 ES6 模塊,下面有一些有趣的觀點:

HTTP/2 會讓模塊打包過期嗎?

對於 HTTP/1,每一個TCP鏈接只容許一個請求。這就是爲何加載多個資源須要多個請求。有了 HTTP/2,一切都變了。HTTP/2 是徹底多路複用的,這意味着多個請求和響應能夠並行發生。所以,咱們能夠在一個鏈接上同時處理多個請求。

因爲每一個 HTTP 請求的成本明顯低於HTTP/1,所以從長遠來看,加載一組模塊不會形成很大的性能問題。一些人認爲這意味着模塊打包再也不是必要的,這固然有可能,但這要具體狀況具體分析了。

例如,模塊打包還有 HTTP/2 沒有好處,好比移除冗餘的導出模塊以節省空間。 若是你正在構建一個性能相當重要的網站,那麼從長遠來看,打包可能會爲你帶來增量優點。 也就是說,若是你的性能需求不是那麼極端,那麼經過徹底跳過構建步驟,能夠以最小的成本節省時間。

總的來講,絕大多數網站都用上 HTTP/2 的那個時候離咱們如今還很遠。我預測構建過程將會保留,至少在近期內。

CommonJS、AMD 與 UMD 會被淘汰嗎?

一旦 ES6 成爲模塊標準,咱們還須要其餘非原生模塊規範嗎?

我以爲還有。

Web 開發遵照一個標準方法進行導入和導出模塊,而不須要中間構建步驟——網頁開發長期受益於此。但 ES6 成爲模塊規範須要多長時間呢?

機會是有,但得等一段時間 。

再者,衆口難調,因此「一個標準的方法」可能永遠不會成爲現實。

總結

但願這篇文章能幫你理清一些開發者口中的模塊和模塊打包的相關概念,共進步。

原文:

https://medium.freecodecamp.o...

https://medium.freecodecamp.o...

你的點贊是我持續分享好東西的動力,歡迎點贊!

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索