用了這麼久的webpack,你不會還沒掌握原理?
一、基本要素
1、Entry/Output
1.1、單入口配置
module.exports = {
entry: './src/index.js', // 打包的入口檔案
output: './dist/main.js', // 打包的輸出
};
1.2、多入口配置
const path = require('path');
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {、
filename: '[name].[hash].js', //通過佔位符確保檔名稱的唯一,可選擇設定hash
path: path.join(__dirname, 'dist'),
// publicPath用於設定載入靜態資源的baseUrl,例如prod模式下指向cdn,dev模式下指向本地服務
publicPath: process.env.NODE_ENV === 'production' ? `//cdn.xxx.com` : '/', //
},
};
2、Loaders
Loaders
函式接收檔案型別作為引數,返回轉換的結果。目前webpack
支援的兩種型別分別為JS
和JSON
,其它型別均需轉換
2.1、通配Loaders
module:{
rules:[
{test:/.\(js|jsx|ts|tsx)$/,use:'ts-loader'} // 例如ts使用ts-loader
]
},
2.2、內聯Loaders
Loaders
還可以直接內聯到程式碼中使用:
import 'style-loader!css-loader!less-loader!./style.less';
2.3、多個Loaders
多個 Loaders
之間執行順序是和 rules
配置相反的,即從右向左執行
2.3.1、原始碼邏輯
loader
先進後出,對應出棧順序從右向左
if (matchResourceData === undefined) {
for (const loader of loaders) allLoaders.push(loader);
for (const loader of normalLoaders) allLoaders.push(loader);
} else {
for (const loader of normalLoaders) allLoaders.push(loader);
for (const loader of loaders) allLoaders.push(loader); // 入棧
}
for (const loader of preLoaders) allLoaders.push(loader); // pre loaders入棧
2.3.2、更改順序
通過配置 enforce
改變執行順序,enforce
有四個列舉值,其執行順序是pre
、normal
、inline
、post
module:{
rules:[
{
test:/\.less$/,
loader:'less-loader',
enforce:'pre' // 預處理
},
{
test: /\.less$/,
loader:'css-loader',
enforce:'normal' // 預設是normal
},
{
test: /\.less$/,
loader:'style-loader',
enforce:'post' // 後處理
},
]
},
3、Plugins
Plugins
負責優化bundle
檔案、資源管理和環境變數注入,webpack
內建了很多 plugin
。例如 DefinePlugin
全域性變數注入外掛、IgnorePlugin
排除檔案外掛、ProgressPlugin
打包進度條外掛等
plugins: [new HtmlwebpackPlugin({ template: './src/index.html' })];
4、Mode
指定當前的構建環境,有三個選項,分別是:production
、development
和none
,當 mode
是 production
時會啟用內建優化外掛,比如TreeShaking
、ScopeHoisting
、壓縮外掛等
module.exports = {
mode: 'production', // 會寫入到環境變數NODE_ENV
};
也可以通過 webpack cli
引數設定
webpack --mode=production
二、熱更新
1、更新流程
1.1、啟動階段 1 -> 2 -> A -> B
- 通過
WebpackCompile
將JS
檔案進行編譯成Bundle
- 將
Bundle
檔案執行在Bundle Server
,使得檔案可通過localhost://xxx
訪問 - 接著構建輸出
bundle.js
檔案給到瀏覽器
1.2、熱更新階段 1 -> 2 -> 3 -> 4
WebpackCompile
將JS
檔案進行編譯成Bundle
- 將
Bundle
檔案執行在HMR Server
- 一旦磁盤裡面的檔案修改,就將有修改的資訊輸出給
HMR Runtime
- 接著
HMR Runtime
區域性更新檔案的變化
2、配置方式
2.1、WDS + HotMoudleReplacementPlugin
2.1.1、WDS(webpack-dev-server)
WDS
提供了 bundle server
的能力,不輸出檔案,而是放在記憶體中,即生成的 bundle.js
檔案可以通過 localhost://xxx
的方式去訪問,同時它提供的livereload
能力,使得瀏覽器能夠自動重新整理
// package.json
"scripts":{
"dev":"webpack-dev-server --open"
}
2.1.2、HotMoudleReplacementPlugin 外掛
HotMoudleReplacementPlugin
外掛給 WDS
提供了熱更新的能力,源自它擁有區域性更新頁面能力的HMR Runtime
。一旦磁盤裡面的檔案修改,HMR Server
就將有修改的js module
資訊傳送給HMR Runtime
// webpack.dev.js 僅在開發環境使用
module.exports = {
mode: 'development',
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
contentBase: './dist', //服務基礎目錄
hot: true, //開啟熱更新
},
};
2.1.3、互動邏輯
監聽到檔案修改時,HotMoudleReplacementPlugin
會生成一個 mainifest
和 update file
,其中 mainifest
描述了發生變化的 modules
,緊接著webpack-dev-server
通過 websocket
通知 client
更新程式碼,client
使用 jsonp
請求 server
獲取更新後的程式碼
2.2、WDM(webpack-dev-middleware)
WDM
將 webpack
輸出的檔案傳輸給伺服器,適用於靈活的定製場景
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
}),
);
app.listen(3000, function () {
console.log('listening on port 3000');
});
三、檔案指紋
檔案指紋主要用於版本管理,表現於打包後文件名的字尾,如xxx//xxx_51773db.js
中的51773db
1、三種類型
型別 | 含義 |
---|---|
Hash | 和整個專案的構建相關,只要專案檔案有修改,整個專案構建的 hash 值就會更改 |
Chunkhash | 和 webpack 打包的 chunk 有關,不同的 entry 會生成不同的 chunkhash 值 |
Contenthash | 根據檔案內容來定義 hash,檔案內容不變,則 contenthash 不變 |
2、常用場景
- 設定
output
的filename
,使用[chunkhash]
filename: '[name][chunkhash:8].js';
- 設定
MiniCssExtractPlugin
的filename
,使用[contenthash]
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`,
});
- 設定
file-loader
的name
,使用[hash]
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name][hash:8].[ext]',
},
},
],
},
];
// 佔位符解釋:[name]:檔名稱,[ext]:資源字尾名
注意喔:hash是由程式碼和路徑生成的。因此相同的程式碼在多臺機器打包部署 hash 會不同,導致資源載入 404。一般通過一臺機器打包,分發部署到不同機器
四、SourceMap
1、開啟配置
開發環境開啟,線上環境關閉。線上排查問題的時候可以將 source map
上傳到錯誤監控系統
module.exports = {
devtool: 'source-map',
};
2、型別
型別 | 說明 |
---|---|
cheap-source-map | 沒有列號,只有行號,速度快 |
cheap-module-source-map | 優化後的 cheap-source-map,避免 babel 等編譯過程式碼行號對不上 |
eval | 通過內聯程式碼 eval 函式 baseURL 確定程式碼路徑 |
eval-source-map | sourcemap 放在 eval 函式後 |
inline-source-map | 放在打包程式碼最後 |
3、檔案格式
利用 mappings
對映表和 names
、sourcesContent
就可以還原出原始碼字串
{
"version": 3, // Source Map版本
"file": "out.js", // 輸出檔案(可選)
"sourceRoot": "", // 原始檔根目錄(可選)
"sources": ["foo.js", "bar.js"], // 原始檔列表
"sourcesContent": [null, null], // 源內容列表(可選,和原始檔列表順序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符號名稱列表
"mappings": "A,AAAB;;ABCDE;" // 帶有編碼對映資料的字串
}
五、TreeShaking
- 程式碼不會被執行,不可到達
- 程式碼執行的結果不會被用到
- 程式碼只會影響死變數(只寫不讀)
TreeShaking
會將以上視為廢棄的程式碼在uglify
階段消除
當
mode
設定為production
的情況下,是預設開啟的。通過在.babelrc
裡設定modules:false
進行取消
TreeShaking
是利用 ES6
模組的特點進行清除
import
只能作為模組頂層的語句出現,且模組名只能是字串常量
import
匯入模組是靜態載入,其獲取的是變數引用,即當模組內部變更時,import
出的變數也會變更。因此 import
不能出現在條件、函式等語句中( export
類似),而 commonjs
中 require
獲取的是模組的快取
import binding
是immutable
的
六、模組機制
webpack
打包後,會給模組加上一層包裹,import
會被轉換成__webpack_require
1、匿名閉包
webpack
打包後是一個匿名閉包,接收的引數 modules
是一個數組,每一項是一個模組初始化函式。通過__webpack_require
載入模組,並返回modules.exports
,
modules
的每個模組成員都是用 __webpack_require__
載入的,installedModules
是載入模組的快取,如果已經__webpack_require__
載入過無需再次載入。
2、ScopeHoisting
構建後的程式碼存在大量的閉包程式碼,導致執行時建立的函式作用域增多,記憶體開銷大,ScopeHoisting
將所有模組的程式碼按照引用順序放在一個函式作用域裡,然後適當的重新命名一些變數以防止變數名衝突,從而減少函式宣告程式碼和記憶體開銷
七、SSR
對SEO
友好的服務端渲染SSR
的核心是減少請求,從而減少白屏時間。其實現原理是:服務端通過react-dom/server
的renderToString
方法將React
元件渲染成字串,返回路由對應的模版。協助的客戶端通過打包,生成針對服務端的元件
renderToString
攜帶有 data-reactid
屬性可配合 hydrate
使用,會複用之前節點只進行事件繫結從而優化首次渲染速度。類似的方法還有 renderToStaticMarkup
1、相容問題
1.1、瀏覽器的全域性變數
node.js
中沒有document
和window
,需通過打包環境進行適配
在 react ssr
應用中,讀取 document
和 window
可以在 useEffect
或 componentDidMount
中進行,當 nodejs
渲染時就會跳過這些執行,避免報錯
- 使用
isomorphic-fetch
或axios
替換fetch
和xhr
1.2、樣式問題
node.js
無法解析css
,可使用ignore-loader
忽略 css 的解析
對於 antd
元件庫,在babel-plugin-import
設定 style
為false
- 使用
isomorphic-style-loader
替換style-loader
2、兩端協作
使用打包後的HTML
為模板,服務端獲取資料後替換佔位符
<body>
<div id="root">
<!--HTML_PLACEHOLDER-->
</div>
<!--INITIAL_DATA_PLACEHOLDER-->
</body>
八、常見優化措施
1、程式碼壓縮
1.1、JS 檔案的壓縮
-
內建了
uglifyjs-webpack-plugin
-
CommonsChunkPlugin
提取chunks
中的公共模組減少總體積
1.2、CSS 檔案的壓縮
-
使用
optimize-css-assets-webpack-plugin
,同時使用cssnano
-
extract-text-webpack-plugin
將css
從產物中分離。
1.3、html 檔案的壓縮
html-webpack-plugin
通常用來定義 html
模板,也可以設定壓縮 minify
引數(production
模式下自動設定 true
)
1.4、圖片壓縮
使用image-webpack-loader
2、自動清理構建目錄
利用 CleanWebpackPlugin
自動清理 output
指定的輸出目錄
3、靜態資源內聯
首屏渲染的樣式儘量選擇內聯或使用 styled-components
。資源內聯可減少請求數,可避免首屏頁面閃動,可進行相關上報打點,可初始化指令碼
3.1、程式碼層面
- raw-loader:js/html 內聯
- style-loader: css 內聯
3.2、請求層面
-
url-loader:小圖片或字型內聯
-
file-loader:可以解析專案中的 url 引入路徑,修改打包後文件引用路徑,指向輸出的檔案。
4、基礎庫分離
4.1、HtmlWebpackExternalsPlugin
將基礎包通過cdn
,而不壓縮排bundle
中
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: '//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123',
global: 'React',
},
],
}),
];
4.2、SplitChunksPlugin
可將公共指令碼、基礎包以及頁面公共檔案分離
splitChunks:{
chunks:'async',// async:非同步引入的庫進行分離(預設) initial:同步引入的庫進行分離 all:所有引入的庫進行分離(推薦)
...
cacheGroups:{
// 1、公共指令碼分離
vendors:{
test:/[\\/]node_modules[\\/]/,
priority:-10
},
// 2、基礎包分離
commons:{
test:/(react|react-dom)/,
name:'vendors',
chunks:'all'
},
// 3、頁面公共檔案分離
commons:{
name:'commons',
chunks:'all',
minChunks:2
}
}
}
4.3、分包
plugins: [
// 使用DLLPlugin進行分包
new webpack.DLLPlugin({
name: '[name]',
path: './build/library/[name].json',
}),
// DllReferencePlugin 對 manifest.json引用
new webpack.DllReferencePlugin({
manifest: require('./build/library/manifest.json'),
}),
];
5、多程序多例項構建
多程序多例項構建,換句話說就是:每次webpack
解析一個模組,將它及它的依賴分配給worker
執行緒中,比如HappyPack
、ThreadLoader
6、快取
- 開啟快取:
babel-loader
、terser-webpack-plugin
- 使用
cache-loader
、hard-source-webpack-plugin
7、縮小構建目標、減少檔案搜尋範圍
- 合理配置
loader
的test
,使用include
來縮小loader
處理檔案範圍
module.exports = {
module: {
rules: [
{
test: /\.js$/, // 尾部補充$號表示尾部匹配
use: ['babel-loader?cacheDirectory'], // babel-loader 通過 cacheDirectory 選項開啟快取
include: path.resolve(__dirname, 'src'), // 只處理src目錄下程式碼,極大提升編譯速度。(如果node_modules下有未編譯過的庫,這裡不建議開啟)
},
],
},
};
- 優化 resolve 配置:
module.exports = {
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 使用絕對路徑指明第三方模組存放的位置,以減少搜尋步驟
extensions: ['.js', '.json'], // extensions儘量少,減少檔案查詢次數
noParse: [/\.min\.js$/], // noParse可以忽略模組的依賴解析,對於min.js檔案一般已經打包好了
},
};
九、可維護的 webpack 構建配置
1、多個配置檔案管理不同環境的 webpack 配置
1.1、通過webpack-merge
合併配置
merge = require('webpack-merge');
module.exports = merge(baseConfig, devConfig);
2、webpack 構建分析
2.1、日誌分析
在package.json
檔案的構建統計資訊欄位新增stats
"scripts":{
"build:stats":"webpack --env production --json > stats.json"
}
2.2、速度分析
利用 speedMeasureWebpackPlugin
分析整個打包總耗時和每個外掛和loader
的耗時情況
const speedMeasureWebpackPlugin = require("speed-measure-webpack-plugin")
const smp = new speedMeasureWebpackPlugin()
const webpackConfig = smp.wrap({
plugins:[
new MyPlugin()
...
]
})
2.3、體積分析
利用bundleAnalyzerPlugin
分析依賴的第三方模組檔案大小和業務裡面的元件程式碼大小,構建完成後會在 8888 埠展示
const bundleAnalyzerPlugin = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new bundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: 'localhost',
analyzerPort: 8888, // 埠號
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false, // 是否輸出到靜態檔案
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info',
}),
],
};
2.4、編譯時進度分析
利用ProgressPlugin
分析編譯進度和模組處理細節
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProgressPlugin({
activeModules: false,
entries: true,
handler(percentage, message, ...args) {
// 列印實時處理資訊
console.info(percentage, message, ...args);
},
modules: true,
modulesCount: 5000,
profile: false,
dependencies: true, // 顯示正在進行的依賴項計數訊息
dependenciesCount: 10000,
percentBy: null,
}),
],
};