概念
webpack 是一個現代的 JavaScript 應用程式的子產品打包器(module bundler)。當 webpack 處理應用程式時,它會遞歸地建構一個依賴關系圖表(dependency graph),其中包含應用程式需要的每個子產品,然後将所有這些子產品打包成少量的 bundle - 通常隻有一個,由浏覽器加載。
學習 webpack,需要先了解幾個核心概念,下面會一一道來。
子產品化(module)
在
子產品化程式設計中,開發者将程式分解相對獨立的代碼塊,并稱之為子產品。
每個子產品具有比完整程式更小的接觸面,使得校驗、調試、測試輕而易舉。 精心編寫的子產品提供了可靠的抽象和封裝界限,使得應用程式中每個子產品都具有條理清楚的設計和明确的目的。
Node.js 從最一開始就支援子產品化程式設計。然而,在 web,子產品化的支援正緩慢到來。在 web 存在多種支援 JavaScript 子產品化的工具,這些工具各有優勢和限制。webpack 基于從這些系統獲得的經驗教訓,并将子產品的概念應用于項目中的任何檔案。
什麼是 webpack 子產品
對比
Node.js 子產品,webpack 子產品能夠以各種方式表達它們的依賴關系,幾個例子如下:
- ES2015
語句import
- CommonJS
require()
- AMD
和define
require
- css/sass/less 檔案中的
。@import
- 樣式(
)或 HTML 檔案(``)中的圖檔連結(image url)url(...)
webpack 1 需要特定的 loader 來轉換 ES 2015 import
,然而 webpack 2 天然支援。
支援的子產品類型
webpack 通過 loader 可以支援各種語言和預處理器編寫子產品。loader 描述了 webpack 如何處理 非 JavaScript(non-JavaScript) 子產品,并且在bundle中引入這些依賴。 webpack 社群已經為各種流行語言和語言處理器建構了 loader,包括:
總的來說,webpack 提供了可定制的、強大和豐富的 API,允許任何技術棧使用 webpack,保持了在你的開發、測試和生成流程中無侵入性(non-opinionated)。
配置檔案 - webpack.config.js
webpack 是高度可配置的,如何子產品化打包、加載都可以基于配置檔案定制。
webpack 的預設配置檔案是
webpack.config.js
因為 webpack 配置是标準的 Node.js CommonJS 子產品,你可以使用如下特性:
- 通過
導入其他檔案require(...)
-
使用 npm 的工具函數require(...)
- 使用 JavaScript 控制流表達式,例如
操作符?:
- 對常用值使用常量或變量
- 編寫并執行函數來生成部配置設定置
依賴圖表(Dependency Graph)
任何時候,一個檔案依賴于另一個檔案,webpack 就把此視為檔案之間有依賴關系。這使得 webpack 可以接收非代碼資源(non-code asset)(例如圖像或 web 字型),并且可以把它們作為依賴提供給你的應用程式。
webpack 從指令行或配置檔案中定義的一個子產品清單開始,處理你的應用程式。 從這些入口起點開始,webpack 遞歸地建構一個依賴圖表,這個依賴圖表包含着應用程式所需的每個子產品,然後将所有這些子產品打包為少量的 bundle- 通常隻有一個 - 可由浏覽器加載。
對于 HTTP/1.1 用戶端,由 webpack 打包你的應用程式會尤其強大,因為在浏覽器發起一個新請求時,它能夠減少應用程式必須等待的時間。對于 HTTP/2,你還可以使用代碼拆分(Code Splitting)以及通過 webpack 打包來實作 最佳優化
入口(entry)
webpack 将建立所有應用程式的依賴關系圖表(dependency graph)。圖表的起點被稱之為入口起點(entry point)。入口起點告訴 webpack 從哪裡開始,并遵循着依賴關系圖表知道要打包什麼。可以将您應用程式的入口起點認為是根上下文(contextual root)或 app 第一個啟動檔案。
在 webpack 中,我們使用
webpack 配置對象(webpack configuration object)中的
entry
屬性來定義入口。
例:
module.exports = {
entry: './path/to/my/entry/file.js'
};
輸出(output)
将所有的資源(assets)歸攏在一起後,還需要告訴 webpack 在哪裡打包應用程式。webpack 的
output
屬性描述了如何處理歸攏在一起的代碼(bundled code)。
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
加載(loader)
webpack 的目标是,讓 webpack 聚焦于項目中的所有資源(asset),而浏覽器不需要關注考慮這些(這并不意味着資源(asset)都必須打包在一起)。webpack 把
每個檔案(.css, .html, .scss, .jpg, etc.) 都作為子產品處理。然而 webpack 隻了解 JavaScript。
webpack loader 會将這些檔案轉換為子產品,而轉換後的檔案會被添加到依賴圖表中。
在更高層面,webpack 的配置有兩個目标。
- 識别出(identify)應該被對應的 loader 進行轉換(transform)的那些檔案
- 由于進行過檔案轉換,是以能夠将被轉換的檔案添加到依賴圖表(并且最終添加到 bundle 中)(
屬性)use
const path = require('path');
const config = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{test: /\.(js|jsx)$/, use: 'babel-loader'}
]
}
};
module.exports = config;
插件(plugins)
由于 loader 僅在每個檔案的基礎上執行轉換,而
插件(plugins)
最常用于(但不限于)在打包子產品的“compilation”和“chunk”生命周期執行操作和自定義功能
(檢視更多)。webpack 的插件系統
極其強大和可定制化想要使用一個插件,你隻需要
require()
它,然後把它添加到
plugins
數組中。多數插件可以通過選項(option)自定義。你也可以在一個配置檔案中因為不同目的而多次使用同一個插件,你需要使用
new
建立執行個體來調用它。
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins
const path = require('path');
const config = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{test: /\.(js|jsx)$/, use: 'babel-loader'}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
module.exports = config;
熱替換(Hot Module Replacement)
子產品熱替換功能會在應用程式運作過程中替換、添加或删除
子產品,而無需重新加載頁面。這使得你可以在獨立子產品變更後,無需重新整理整個頁面,就可以更新這些子產品,極大地加速了開發時間。
這一切是如何運作的?
站在 App 的角度
- app 代碼要求 HMR runtime 檢查更新。
- HMR runtime (異步)下載下傳更新,然後通知 app 代碼更新可用。
- app 代碼要求 HMR runtime 應用更新。
- HMR runtime (異步)應用更新。
你可以設定 HMR,使此程序自動觸發更新,或者你可以選擇要求在使用者互動後進行更新。
站在編譯器(webpack) 的角度
除了普通資源,編譯器(compiler)需要發出 "update",以允許更新之前的版本到新的版本。"update" 由兩部分組成:
- 待更新 manifest (JSON)
- 一個或多個待更新 chunk (JavaScript)
manifest 包括新的編譯 hash 和所有的待更新 chunk 目錄。
每個待更新 chunk 包括用于與所有被更新子產品相對應 chunk 的代碼(或一個 flag 用于表明子產品要被移除)。
編譯器確定子產品 ID 和 chunk ID 在這些建構之間保持一緻。通常将這些 ID 存儲在記憶體中(例如,當使用
webpack-dev-server時),但是也可能将它們存儲在一個 JSON 檔案中。
站在子產品的角度
HMR 是可選功能,隻會影響包含 HMR 代碼的子產品。舉個例子,通過
style-loader
為 style 樣式追加更新檔。 為了運作追加更新檔,
style-loader
實作了 HMR 接口;當它通過 HMR 接收到更新,它會使用新的樣式替換舊的樣式。
類似的,當在一個子產品中實作了 HMR 接口,你可以描述出當子產品被更新後發生了什麼。然而在多數情況下,不需要強制在每個子產品中寫入 HMR 代碼。如果一個子產品沒有 HMR 處理函數,更新就會冒泡。這意味着一個簡單的處理函數能夠對整個子產品樹(complete module tree)進行處理。如果在這個子產品樹中,一個單獨的子產品被更新,那麼整個子產品樹都會被重新加載(隻會重新加載,不會遷移)。
站在 HMR Runtime 的角度 (Technical)
對于子產品系統的 runtime,附加的代碼被發送到
parents
children
跟蹤子產品。
在管理方面,runtime 支援兩個方法
check
apply
check
發送 HTTP 請求來更新 manifest。如果請求失敗,說明沒有可用更新。如果請求成功,待更新 chunk 會和目前加載過的 chunk 進行比較。對每個加載過的 chunk,會下載下傳相對應的待更新 chunk。當所有待更新 chunk 完成下載下傳,就會準備切換到
ready
狀态。
apply
方法将所有被更新子產品标記為無效。對于每個無效子產品,都需要在子產品中有一個更新處理函數,或者在它的父級子產品們中有更新處理函數。否則,無效标記冒泡,并将父級也标記為無效。每個冒泡繼續直到到達應用程式入口起點,或者到達帶有更新處理函數的子產品(以最先到達為準)。如果它從入口起點開始冒泡,則此過程失敗。
之後,所有無效子產品都被(通過 dispose 處理函數)處理和解除加載。然後更新目前 hash,并且調用所有 "accept" 處理函數。runtime 切換回
閑置
狀态,一切照常繼續。
産生的檔案 (Technical)
左側表示初始編譯器通過。右側表示更新了子產品 4 和 9 。

它能夠用于?
你可以在開發過程中将 HMR 作為 LiveReload 的替代。
支援熱模式,在試圖重新加載整個頁面之前,熱模式會嘗試使用 HMR 來更新。檢視如何實作
在 React 項目中使用 HMR為例。
一些 loader 已經生成可熱更新的子產品。例如,
style-loader
能夠置換出頁面的樣式表。對于這樣的子產品,你不需要做任何特殊處理。
webpack 的強大之處在于它的可定制化,取決于特定項目需求,這裡有許多配置 HMR 的方式。
Webpack 系列教程
歡迎閱讀其它内容: