webpack究竟解決了什麼

webpack最初的目標就是實現前端項目的木塊化, 也就是說他所解決的問題是如何在前端項目中高效的管理和維護項目中的每一個資源。

如果要搞明白webpack, 就必須先對他想要解決的問題或者目標有一個充分的認識, 帶着問題再去理解他的很多特性, 學習思路會更清晰, 理解也會更深刻。

模塊化的演化過程

隨着互聯網的深入發展, 前端技術標準發生了巨大的改變。 早起的前端技術標準根本沒有預料到前端行業會有今天這個規模, 所以在設計上存在很多的缺陷, 導致我們現在去實現前端模塊化時 都會遇到諸多問題。雖然說 如今絕大部分問題都已經被一些標準或者工具解決了, 但在這個演進過程中 依然存在很多值得我們學習和思考。

stage-1 文件劃分方式

└─ stage-1
    ├── module-a.js    
    ├── module-b.js    
    └── index.html
// module-a.js 
function foo () {
   console.log('moduleA#foo') 
}
// module-b.js 
var data = 'something'
<!DOCTYPEhtml>
<html>
    <head>
        <metacharset="UTF-8">
        <title>Stage1</title>
    </head>
<body>
    <scriptsrc="module-a.js"></script>
    <scriptsrc="module-b.js"></script>
    <script>
        //直接使用全局成員
        foo()
        //可能存在命名衝突
        console.log(data)data='other'
        //數據可能會被修改
    </script>
<body>
<html>

缺點:

  • 模塊直接在全局工作,大量模塊成員污染全局作用域;

  • 沒有私有空間,所有模塊內的成員都可以在模塊外部被訪問或者修改;

  • 一旦模塊增多,容易產生命名衝突;

  • 無法管理模塊與模塊之間的依賴關係;

  • 在維護的過程中也很難分辨每個成員所屬的模塊;

Stage 2 – 命名空間方式

後來,我們約定每個模塊只暴露一個全局對象,所有模塊成員都掛載到這個全局對象中,具體做法是在第一階段的基礎上,通過將每個模塊“包裹”爲一個全局對象的形式實現,這種方式就好像是爲模塊內的成員添加了“命名空間”,所以我們又稱之爲命名空間方式。

// module-a.js
window.moduleA = {
  method1: function () {
    console.log('moduleA#method1')
  }
}
// module-b.js
window.moduleB = {
  data: 'something'
  method1: function () {
    console.log('moduleB#method1')
  }
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Stage 2</title>
</head>
<body>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
    // 模塊成員依然可以被修改
    moduleA.data = 'foo'
  </script>
</body>
</html>

這種命名空間的方式只是解決了命名衝突的問題,但是其它問題依舊存在。

 

 Stage 3 – IIFE

使用立即執行函數表達式(IIFE,Immediately-Invoked Function Expression)爲模塊提供私有空間。具體做法是將每個模塊成員都放在一個立即執行函數所形成的私有作用域中,對於需要暴露給外部的成員,通過掛到全局對象上的方式實現。

// module-a.js
;(function () {
  var name = 'module-a'

  function method1 () {
    console.log(name + '#method1')
  }

  window.moduleA = {
    method1: method1
  }
})()
// module-b.js
;(function () {
  var name = 'module-b'

  function method1 () {
    console.log(name + '#method1')
  }

  window.moduleB = {
    method1: method1
  }
})()

這種方式帶來了私有成員的概念,私有成員只能在模塊成員內通過閉包的形式訪問,這就解決了前面所提到的全局作用域污染和命名衝突的問題。

Stage 4 - IIFE 依賴參數

在 IIFE 的基礎之上,我們還可以利用 IIFE 參數作爲依賴聲明使用,這使得每一個模塊之間的依賴關係變得更加明顯。

// module-a.js
;(function ($) { // 通過參數明顯表明這個模塊的依賴
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
    $('body').animate({ margin: '200px' })
  }

  window.moduleA = {
    method1: method1
  }
})(jQuery)

模塊加載的問題

以上 4 個階段是早期的開發者在沒有工具和規範的情況下對模塊化的落地方式,這些方式確實解決了很多在前端領域實現模塊化的問題,但是仍然存在一些沒有解決的問題。

<!DOCTYPE html>
<html>
<head>
  <title>Evolution</title>
</head>
<body>
  <script src="https://unpkg.com/jquery"></script>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
  </script>
</body>
</html>

最明顯的問題就是:模塊的加載。在這幾種方式中雖然都解決了模塊代碼的組織問題,但模塊加載的問題卻被忽略了,我們都是通過 script 標籤的方式直接在頁面中引入的這些模塊,這意味着模塊的加載並不受代碼的控制,時間久了維護起來會十分麻煩。試想一下,如果你的代碼需要用到某個模塊,如果 HTML 中忘記引入這個模塊,又或是代碼中移除了某個模塊的使用,而 HTML 還忘記刪除該模塊的引用,都會引起很多問題和不必要的麻煩。

更爲理想的方式應該是在頁面中引入一個 JS 入口文件,其餘用到的模塊可以通過代碼控制,按需加載進來。

模塊化規範的出現

除了模塊加載的問題以外,目前這幾種通過約定實現模塊化的方式,不同的開發者在實施的過程中會出現一些細微的差別,因此,爲了統一不同開發者、不同項目之間的差異,我們就需要制定一個行業標準去規範模塊化的實現方式。

再接合我們剛剛提到的模塊加載的問題,我們現在的需求就是兩點:

  • 一個統一的模塊化標準規範
  • 一個可以自動加載模塊的基礎庫

提到模塊化規範,你可能會想到 CommonJS 規範,它是 Node.js 中所遵循的模塊規範,該規範約定,一個文件就是一個模塊,每個模塊都有單獨的作用域,通過 module.exports 導出成員,再通過 require 函數載入模塊。現如今的前端開發者應該對其有所瞭解,但是如果我們想要在瀏覽器端直接使用這個規範,那就會出現一些新的問題。

如果你對 Node.js 的模塊加載機制有所瞭解,那麼你應該知道,CommonJS 約定的是以同步的方式加載模塊,因爲 Node.js 執行機制是在啓動時加載模塊,執行過程中只是使用模塊,所以這種方式不會有問題。但是如果要在瀏覽器端使用同步的加載模式,就會引起大量的同步模式請求,導致應用運行效率低下。

所以在早期制定前端模塊化標準時,並沒有直接選擇 CommonJS 規範,而是專門爲瀏覽器端重新設計了一個規範,叫做 AMD ( Asynchronous Module Definition) 規範,即異步模塊定義規範。同期還推出了一個非常出名的庫,叫做 Require.js,它除了實現了 AMD 模塊化規範,本身也是一個非常強大的模塊加載器。

在 AMD 規範中約定每個模塊通過 define() 函數定義,這個函數默認可以接收兩個參數,第一個參數是一個數組,用於聲明此模塊的依賴項;第二個參數是一個函數,參數與前面的依賴項一一對應,每一項分別對應依賴項模塊的導出成員,這個函數的作用就是爲當前模塊提供一個私有空間。如果在當前模塊中需要向外部導出成員,可以通過 return 的方式實現。

除此之外,Require.js 還提供了一個 require() 函數用於自動加載模塊,用法與 define() 函數類似,區別在於 require() 只能用來載入模塊,而  define() 還可以定義模塊。當 Require.js 需要加載一個模塊時,內部就會自動創建 script 標籤去請求並執行相應模塊的代碼。

目前絕大多數第三方庫都支持 AMD 規範,但是它使用起來相對複雜,而且當項目中模塊劃分過於細緻時,就會出現同一個頁面對 js 文件的請求次數過多的情況,從而導致效率降低。在當時的環境背景下,AMD 規範爲前端模塊化提供了一個標準,但這只是一種妥協的實現方式,並不能成爲最終的解決方案。

同期出現的規範還有淘寶的 Sea.js,只不過它實現的是另外一個標準,叫作 CMD,這個標準類似於 CommonJS,在使用上基本和 Require.js 相同,可以算上是重複的輪子。但隨着前端技術的發展,Sea.js 後來也被 Require.js 兼容了。如果你感興趣可以課後瞭解一下 

 

模塊化的標準規範

儘管上面介紹的這些方式和標準都已經實現了模塊化,但是都仍然存在一些讓開發者難以接受的問題。

隨着技術的發展,JavaScript 的標準逐漸走向完善,可以說,如今的前端模塊化已經發展得非常成熟了,而且對前端模塊化規範的最佳實踐方式也基本實現了統一。

  • 在 Node.js 環境中,我們遵循 CommonJS 規範來組織模塊。
  • 在瀏覽器環境中,我們遵循 ES Modules 規範。

而且在最新的 Node.js 提案中表示,Node 環境也會逐漸趨向於 ES Modules 規範,也就是說作爲現階段的前端開發者,應該重點掌握 ES Modules 規範。

因爲 CommonJS 屬於內置模塊系統,所以在 Node.js 環境中使用時不存在環境支持問題,只需要直接遵循標準使用 require 和 module 即可。

但是對於 ES Modules 規範來說,情況會相對複雜一些。我們知道 ES Modules 是 ECMAScript 2015(ES6)中才定義的模塊系統,也就是說它是近幾年才制定的標準,所以肯定會存在環境兼容的問題。在這個標準剛推出的時候,幾乎所有主流的瀏覽器都不支持。但是隨着 Webpack 等一系列打包工具的流行,這一規範纔開始逐漸被普及。

經過 5 年的迭代, ES Modules 已發展成爲現今最主流的前端模塊化標準。相比於 AMD 這種社區提出的開發規範,ES Modules 是在語言層面實現的模塊化,因此它的標準更爲完善也更爲合理。而且目前絕大多數瀏覽器都已經開始能夠原生支持 ES Modules 這個特性了,所以說在未來幾年,它還會有更好的發展,短期內應該不會有新的輪子出現了。

綜上所述,如何在不同的環境中去更好的使用 ES Modules 將是你重點考慮的問題。

ES Modules 特性

那對於 ES Modules 的學習,可以從兩個維度入手。首先,你需要了解它作爲一個規範或者說標準,到底約定了哪些特性和語法;其次,你需要學習如何通過一些工具和方案去解決運行環境兼容帶來的問題。

 

 

模塊打包工具的出現

模塊化可以幫助我們更好地解決複雜應用開發過程中的代碼組織問題,但是隨着模塊化思想的引入,我們的前端應用又會產生了一些新的問題,比如:

  • 首先,我們所使用的 ES Modules 模塊系統本身就存在環境兼容問題。儘管現如今主流瀏覽器的最新版本都支持這一特性,但是目前還無法保證用戶的瀏覽器使用情況。所以我們還需要解決兼容問題。
  • 其次,模塊化的方式劃分出來的模塊文件過多,而前端應用又運行在瀏覽器中,每一個文件都需要單獨從服務器請求回來。零散的模塊文件必然會導致瀏覽器的頻繁發送網絡請求,影響應用的工作效率。
  • 最後,談一下在實現 JS 模塊化的基礎上的發散。隨着應用日益複雜,在前端應用開發過程中不僅僅只有 JavaScript 代碼需要模塊化,HTML 和 CSS 這些資源文件也會面臨需要被模塊化的問題。而且從宏觀角度來看,這些文件也都應該看作前端應用中的一個模塊,只不過這些模塊的種類和用途跟 JavaScript 不同。

對於開發過程而言,模塊化肯定是必要的,所以我們需要在前面所說的模塊化實現的基礎之上引入更好的方案或者工具,去解決上面提出的 3 個問題,讓我們的應用在開發階段繼續享受模塊化帶來的優勢,又不必擔心模塊化對生產環境所產生的影響。

接下來我們先對這個更好的方案或者工具提出一些設想:

  • 第一,它需要具備編譯代碼的能力,也就是將我們開發階段編寫的那些包含新特性的代碼轉換爲能夠兼容大多數環境的代碼,解決我們所面臨的環境兼容問題。
  • 第二,能夠將散落的模塊再打包到一起,這樣就解決了瀏覽器頻繁請求模塊文件的問題。這裏需要注意,只是在開發階段才需要模塊化的文件劃分,因爲它能夠幫我們更好地組織代碼,到了實際運行階段,這種劃分就沒有必要了。
  • 第三,它需要支持不同種類的前端模塊類型,也就是說可以將開發過程中涉及的樣式、圖片、字體等所有資源文件都作爲模塊使用,這樣我們就擁有了一個統一的模塊化方案,所有資源文件的加載都可以通過代碼控制,與業務代碼統一維護,更爲合理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章