概述
關於本文
最近有機會系統地總結一下 webpack 相關的東西,寫篇文章來記錄一下。寫這篇文章一半是為了自己看,一半給別人看。如果有人看了這篇文章感覺很有用,真是我莫大的榮幸。 首先這不是 《深入淺出 webpack》, 也不是《一篇文章帶你玩轉 webpack》,更不是 《學習 webpack 這一篇就夠了》。
如何學 webpack
看文件是最簡單,最直接的方法,不過官方文件有些地方寫的不太清楚,這時候,那些文章和部落格的作用就比較明顯了,畢竟這些是文章的作者們摸索出來的。比如這篇文章沒有大篇幅複製官方文件,而是力求多介紹些沒有在文件中沒體現的部分。
關於 webpack
webpack 是現在最流行的打包工具,即使現在的 webpack 也可以像 parcel 一樣實現零配置打包;vue-cli3 也提供了開箱即用的零配置打包方案。但是,這些方案不一定能滿足自己的需求。很多時候還需要我們自己根據自己的需求配置 webpack。
安裝
- webpack4.x 需同時安裝 webpack-cli
- 推薦區域性安裝,然後可以配合
npx
命令執行區域性的 webpack 和 webpack-cli
在命令列使用 npx 命令 呼叫 webpack:
npx webpack
npx 會呼叫node_modules/.bin
下的webpack
檔案, 在這個檔案裡,會先呼叫webpack-cli
以讀取命令列引數。
概念
四個核心概念
- 入口(entry),指示 webpack 應該使用哪個模組,來作為構建其內部依賴圖的開始。
- 出口(output),告訴 webpack 在哪裡輸出它所建立的 bundles,以及如何命名這些檔案。
- loader, 將所有型別的檔案,轉換為應用程式的依賴圖(和最終的 bundle)可以直接引用的模組。
- 外掛, 在 webpack 打包過程中增加額外的功能。
bundle, chunck 和 module
官方解釋 大概說明了這幾個概念;
-
從先後順序 module:模組,webpack 模組(專案的資源) chunck: 程式碼塊 (中間產物) bundle: 打包後的結果
-
從“包含” 關係 module <= chunck <= bundle
module
在 webpack 中任何資源都被作為模組處理,webpack 支援 js 各種模組化標準的模組,和通過 loader 轉化來的模組。
chunck
chunkname, webpack 中每個 chunk 都會有一個名稱:
- 如果,entry 是一個 string 或 array, 就只會生成一個 chunk 名稱為 main
- 如果,entry 是 object,就可能會出現多個 chunk,chunk 的名稱是 object 的鍵名。
- 如果是執行時產生的 chunk,如
CommonsChunkPlugin
,mini-css-extract-plugin
外掛產生的 chunk,需要在外掛配置中指定 chunk 的名稱。
bundle
webpack 是個 JavaScript 打包器,bundle 是指 webpack 打包後的結果。
本質上,
webpack
是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當webpack
處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個bundle
。
基本配置
配置檔案為: webpack.config.js
或 webpackfile.js
;
入口
基本配置:
const config = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};
複製程式碼
出口
基礎配置:
{
output: {
filename: '[name].js',
path: '/home/proj/public/assets'
}
}
複製程式碼
注意: path
需為絕對路徑
webpack-dev-server
注意,這是第三方的模組, 需要另外安裝。 webpack-dev-server 不是構建本地開發環境的唯一選擇,其他方式有看這裡。 其實 webpack-dev-server 會啟動一個 express 服務,所以想要更到的自由度,可以直接使用 express 的 webpack-dev-middleware。
devServer
選項用來配置 wepack-dev-server,基本配置:
{
devServer: {
contentBase: path.join(__dirname, 'dist'), // 提供靜態檔案的目錄
compress: true, // gzip 壓縮
publicPath: '/assets/', // bundle 的可訪問路徑
port: 9000
}
}
複製程式碼
live reloading 和 hot module replacement(HMR)
webpack-dev-server 預設已經實現了 live reloading,無需額外配置。 CSS 的 HMR 可以使用 style-loader。 Js 的 HMR ,需要在專案程式碼裡寫過載程式碼。一般藉助開發框架提供的提供的 HMR 方案, 比如 vue 提供的 vue-loader。
模式
webpack4 新增了 mode 選項,可以指定開發模式/生產模式。
{
mode: 'production'
}
複製程式碼
如果值為 development
或 production
, 會啟用 webpack 內建的優化策略。
loader
在 webpack 中使用非 js 模組(或需要特殊處理的 js 模組,比如 ES 模組)時,需要使用 loader 進行轉換;使用 module.rules
配置 loader 的規則:
module: {
rules: [
{
test: /\.html$/,
use: 'html-withimg-loader'
}
]
}
複製程式碼
webpack 的配置比較靈活,很多的配置項都可以接受不同資料型別的值,比如,上面的 test
屬性,可以接受字串、正規表示式,陣列,物件,甚至是函式。
同樣,use
屬性的配置也比較靈活,當只使用一個 loader 時,可以只寫一個字串(如 css-loader
);當使用多個 loader 時,可以寫一個陣列(['css-loader', 'less-loader']
),當需要給每個 loader 一些其他的配置時,可以把上面的字串替換為一個物件:
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 8192,
fallback: 'file-loader',
name: 'img/[name]-[hash].[ext]'
}
}
}
複製程式碼
注意: 多個 loader 的執行順序是:自右向左,自下向上; 更多 loader 。
外掛
配置 webpack,很大一部分是在配置外掛。
基本的做法是,new
一個外掛例項,傳入一些配置資訊:
{
plugins: [
new HtmlWebpackPlugin({
title: 'My App'
})
]
}
複製程式碼
更多外掛及相關配置 看這裡。
常見的幾個外掛;
HtmlWebpackPlugin
,建立 HTML 檔案TerserJSPlugin
,使用UglifyJS
壓縮混淆 js 程式碼MiniCssExtractPlugin
, 提取 CSSCleanWebpackPlugin
, 清除檔案webpack.DefinePlugin
, 定義全域性常量
注意,隨著 webpack 的升級,一些老專案和網上的一些文章的外掛已經過時了,比如:
-
抽離 CSS 為獨立檔案,使用 mini-css-extract-plugin, 而不再建議使用
extract-text-webpack-plugin
。 -
webpack 預設安裝
terser-webpack-plugin
, 不再需要使用uglifyjs-webpack-plugin
。因為後者對 ES6+ 的處理問題。 -
抽離公共程式碼的
CommonsChunkPlugin
現在使用功能更強大的optimization.splitChunks
代替。
模組解析
webpack 與 Nodejs 的模組解析相似,查詢規則基本相同,前者在功能上有所增強。
模組路徑解析相關的配置都在 resolve
欄位下,常用的有:
resolve.alias
, 設定模組別名resolve.extensions
,自動解析確定的副檔名
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'service': path.resolve(__dirname, 'src/js/service'),
'utils': path.resolve(__dirname, 'src/js/utils')
}
}
複製程式碼
source map
js 的 source map
通過 devtool
配置,也可以使用 SourceMapDevToolPlugin
進行更細粒度的配置。
關於 devtool
, 文件 給出了多種方式,每一種的運算速度都不一樣,有的適合開發環境有的適合生產環境,可以根據自己的需求選擇合適的配置。
css 的 source map
需要在樣式處理的各個 loader 中開啟 source map 配置。
優化
loader 優化
loader 可以通過 include
和 exclude
配置來縮小檔案的搜尋範圍。
babel-loader 官方提供了還提供了 cacheDirectory
配置,快取編譯結果,避免重複編譯。
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true
}
}
}
複製程式碼
模組查詢優化
resolve.modules
配置 webpack 去哪個目錄下尋找第三方模組。其預設值為, ['node_modules']
。
和 Nodejs 的模組查詢機制類似,webpack 會先從當前目錄的 ./node_modules
目錄開始查詢想要的模組,如果沒找到,就去 ../node_modules
中查詢,再沒有就去 ../../node_modules
, 以此類推。
一般情況下,我們的第三方模組都是安裝在根目錄的 node_modules
目錄下的,此時可以通過 resolve.modules
指定固定的目錄,以減少尋找。
modules: [path.resolve(__dirname, 'node_modules')]
複製程式碼
resolve.alias
建立 import
或 require
的別名,使模組引入變得更簡單。
有些庫,安裝到 node_modules
目錄下的檔案包括兩部分:
- 模組化的程式碼,使用這些程式碼要經過 webpack 的編譯;
- 已經打包好的單獨的檔案。
當第三方模組的完整性較強且模組入口為模組化程式碼,我們不希望 webpack 再次編譯的時候,可以使用 resolve.alias
將匯入模組換成使用已打包好的檔案。
反之亦然,當只使用模組一部分功能,希望有效利用優化(如 Tree Shaking),且第三方模組入口為已打包的單獨檔案時,可以使用 resolve.alias
將匯入的模組換成使用模組化的程式碼。
resolve.extensions
當匯入預計沒有字尾時,webpack 會自動加上字尾去嘗試詢問檔案是否存在。resolve.extensions
便是用於配置在嘗試過程中用到的字尾名列表。
- 字尾名列表要儘可能短;
- 頻率出現高的檔案字尾名放到前面;
- 在原始碼中,匯入語句時,儘可能加上檔案字尾。
module.noParse
讓 webpack 忽略對部分沒有采用模組化 的檔案的遞迴解析。
動態連結庫
專案中引入的第三方模組,每一次打包都要進行編譯,特別消耗效能。DLLPlugin 提高打包速度的基本原理是,單獨打包一次這些第三方模組,生成一個單獨的檔案,之後在正式打包時就可以直接從這個檔案中引入之前打包好的第三方模組。
注意: DLL 常用於打包第三方模組,但不侷限於第三方模組。
先從基本原理說起,比如我們要單獨打包 A,B 兩個庫:
- 單獨打包出一個 js(動態連結庫),暴露出一個變數,這個變數包含模組 A 和 B 以及其依賴模組,這些模組可以通過其在變數中的 ID 訪問。
- 動態連結庫打包時,需要一個清單檔案來儲存這些依賴的和 ID 的對應關係。
- 在 HTML 中引入單獨打包好的 js 檔案,使其暴露的變數在瀏覽器中可訪問。
- 真正打包專案時,需要去 2 中的清單中查詢是否已經打包,當遇到已經打包過的模組,則不再打包。
對應的配置:
- 要單獨打包動態連結庫 js,一般新建一個單獨的 webpack 配置檔案。為了便於多人合作,這個配置檔案、打包好的動態連結庫及其清單檔案最好都使用版本管理。
- 這個單獨的 webpack 配置文件,同其它 webpack 配置文件基本一樣,最大的不同是要使用 webpack.DLLPlugin 生成清單檔案。
- 瀏覽器中如果要能訪問暴露出的變數,需要在 HTML 中使用
script
標籤引入動態連結庫,最笨的方法手寫(:。 - 真正打包專案時,需要通過 webpack.DllReferencePlugin 外掛來檢查模組是否存在動態連結庫裡。
例子:官方提供的例子
其他玩法
- 可以打包出多個動態連結庫。
- 可以打包更多的資源,如 css。
結合 npm script 或 webpack-html-plugin 可以實現過程更高的自動化。
externals
externals
配置選項提供了「從輸出的 bundle 中排除依賴」的方法。相反,所建立的 bundle 依賴於那些存在於使用者環境(consumer's environment)中的依賴。
externals
的優勢在於,可以從 CND 引入資源,有效利用瀏覽器快取。對於 jQuery 這樣的庫來說 externals
是一個不錯的選擇。
與 DLL(動態連結庫) 對比
以 externals
最終的打包結果(以 jQuery 為例):
jquery: function(module, exports) {
eval('module.exports = jQuery;');
}
複製程式碼
也就是 webpack 對 externals
的處理僅僅是把全域性變數(jQuery
)作為一個依賴模組進行管理,所以 externals
不具備管理依賴的依賴的能力,也就無法避免依賴的依賴重複打包。
DLL 也有類似的結果 (假如以var __dll__vendor
匯出動態連結庫):
'dll-reference __dll__vendor': function(module, exports) {
eval('module.exports = __dll__vendor;');
}
複製程式碼
開啟頁面,可以在控制檯訪問到這些打包進動態連結庫的模組:
__dll__vendor.m[id]; // 通過id可以訪問已經打包進動態連結庫的模組
複製程式碼
與 externals
相同,DLL 也把一個全域性的變數作為一個依賴進行管理。不同的是,這個變數的結構和 webpack 執行時模組管理的結構是一樣的,其中,依賴和依賴的依賴,都可以通過 ID 訪問到,可以避免重複打包。
提取公共程式碼
optimization.splitChunks
選項通過配置 SplitChunksPlugin 來實現提取公共程式碼。一般的應用場景是:
- 提取單/多頁面公共第三方庫程式碼
- 提取單/多頁面公共業務程式碼
webpack 在生產環境的提取公共程式碼策略和以下因素有關:
- 程式碼被多少次共享或是第三方庫
- 程式碼塊大小
- 並行按需載入的請求數量
- 頁面初始化時的並行請求的數量
更多細節請移步官方文件
我們在做公共程式碼的提取相關配置的時候,也是從上面的幾個角度去配置的。
按需載入
即懶載入,在初始化頁面的時候不一次性載入所有程式碼,當需要更多內容時,再對用到的內容進行即時載入。
目前可用寫法:
- ES 規範的
import()
語法(推薦) - 早期 webpack 提供的
require.ensure
import(/* webpackChunkName: "print" */ './print').then(module => {
// ES6 模組的 import() 方法引入模組時,必須指向模組的 .default 值
var print = module.default;
});
複製程式碼
上面程式碼中,/* webpackChunkName: "print" */
是以行內註釋的方式為動態生成的 chunk 賦一個名稱。此處還可以寫更多的引數如:
webpackPrefetch
預取模組webpackPreload
預載入模組
關於上面程式碼中的 module.default
,需要注意:
webpack v4 開始,在 import CommonJS 模組時,不會再將匯入模組解析為
module.exports
的值,而是為 CommonJS 模組建立一個 artificial namespace object(人工名稱空間物件),關於其背後原因的更多資訊,請閱讀 webpack 4: import() 和 CommonJs。
總結來說:
- webpack4 與 webpack3 的
import()
,對 CommonJS 模組處理方式不太一樣; - webpack4 開始,
import()
時,解析的都是一個名稱空間物件了。
CND 加速
把靜態資源上傳到CDN服務上,能獲得更快的響應速度。由於 CDN 服務一般會為資源開啟很長時間的快取,所以HTML檔案一般不放到 CND 上,而且關閉快取。而對於靜態的JavaScript、CSS、圖片等檔案傳到 CDN 上,同時為檔名帶上由檔案內容 計算出的 hash 指紋。
另外瀏覽器在同一時刻對同一域名的並行請求有限制,一般的解決方案是:把這些靜態資源分散到不同域名下的CDN服務上,如:JavaScript檔案放到 js.cdn.xx.com 域名下,圖片放到 img.cdn.xx.com 域名下。
同時多個域名又會增加域名解析的成本,這個也需要權衡。當然也可以通過也可以在HTML的head 標籤中,增加
<link rel="dns-prefetch" href="//js.cdn.xx.com" />
預解析域名,以減少域名解析帶來的延遲。
配置:
- 在
output.publicPath
設定 JavaScript 的基地址。 - MiniCssExtractPlugin.loader 通過
publicPath
設定 CSS 的基地址。 - file-loader/url-loader 通過
publicPath
設定圖片等檔案的的基地址。
檔案 hash 指紋在利用瀏覽器快取中發揮了很重要的作用,在 webpack 中可用的 hash 的型別有 hash, chunkhash 和 contenthash,下面簡要介紹其主要區別。
hash, chunkhash 和 contenthash
- hash,由編譯過程中的 compilation 物件計算得到的 hash,可以理解是整個專案的 hash 值,專案中任何檔案改變都會造成 hash 不同。
- chunkhash 是根據 chunk 內容計算得到的 hash,對於每個 chunk 來說,如果該 chunk 程式碼不變,那麼 hash 也將保持不變。
- contenthash,非webpack提供,而是由 ExtractTextplugin 和 MiniCssExtractPlugin 這些動態建立 chunk 的外掛提供。是由抽離出的檔案的內容計算得到的 hash。
tree-shaking
用來消除死碼。webpack 在生產環境,已經預設開啟了 tree-shaking,注意,為了發揮tree-shaking 的作用,專案中必需使用 ES6 的模組引用和匯出規則。
CSS 的 “tree-shaking” 可以通過 optimize-css-assets-webpack-plugin 實現。
Scope Hoisting
Scope Hoisting 可以讓 Webpack 打包出來的程式碼檔案更小、執行的更快;webpack預設在生產模式已經開啟 Scope Hoisting。
大概的原理: 早期的 webpack 模組打包,把模組打包到一個個作用域隔離的函式中,然後把這些函式表示式放到一個陣列裡,作為引數傳遞給 webpack 執行時模組解析的函式。其中執行這些函式建立了大量作用域,增加了記憶體開銷。 ModuleConcatenationPlugin (webpack內建實現Scope Hoisting的外掛)通過分析出模組之間的依賴關係,可以將一個模組以行內函數的形式注入到另一個模組。
注意,此外掛僅適用於由 webpack 直接處理的 ES6 模組)。在使用轉譯器(transpiler)時,你需要禁用對模組的處理(例如 Babel 中的 modules 選項)。
happyPack
happyPack 可以實現多程式併發處理 loader 解析。 隨著 webpack 效能的提升,happyPack 會被逐漸淘汰。
Is it necessary for Webpack 4?
Short answer: maybe not.
Long answer: there's now a competing add-on in the form of a loader for processing files in multiple threads, exactly what HappyPack does. The fact that it's a loader and not a plugin (or both, in case of H.P.) makes it much simpler to configure. Look at thread-loader and if it works for you - that's great, otherwise you can try HappyPack and see which fares better for you.
以上引自 happypack GitHub 的 FAQ。
現在可以嘗試使用 webpack 提供的 thread-loader, 配置也很簡單。
最後
文中有很多都是個人的觀點,如果有啥不對,大家可以評論區指出。