Webpack 如何協助進行資產快取
接下來 (最佳化應用程式大小, 縮短應用程式的載入時間可用來將應用程式的某些部分 並避免每次都重新下載。
使用軟體包版本管理和快取標頭
常見的快取方法為:
指示瀏覽器快取檔案很長一段時間 (例如一年):
# Server header Cache-Control: max-age=31536000
如果您不熟悉
Cache-Control
的功能,請參閱 Jake Archibald 的 最佳快取相關文章 做法。並重新命名檔案,強制重新下載:
<!-- Before the change --> <script src="./index-v15.js"></script> <!-- After the change --> <script src="./index-v16.js"></script>
這個方法會指示瀏覽器下載 JS 檔案、快取檔案,並使用 快取副本。只有在檔案名稱改變時,瀏覽器才會連線到網路 或是一年後
如果使用 webpack 同樣可以執行相同操作,但您無法指定版本編號,而是指定
檔案雜湊。如要在檔案名稱中加入雜湊,請使用
[chunkhash]
:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
}
};
如果需要
檔案名稱來傳送給用戶端,請使用 HtmlWebpackPlugin
或
WebpackManifestPlugin
。
HtmlWebpackPlugin
是
但較缺乏彈性的做法在編譯期間,這個外掛程式會
包含所有已編譯資源的 HTML 檔案。如果您的伺服器邏輯
對您的問題而言,這應該足以讓您:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
WebpackManifestPlugin
敬上
是一種更有彈性的方法,在伺服器零件複雜時相當實用。
在建構期間,它會產生 JSON 檔案,其中每個檔案名稱之間的對應關係
不含雜湊和檔案名稱。在伺服器上使用這個 JSON 找出答案
要使用的檔案:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
延伸閱讀
- Jake Archibald 特別介紹快取最佳 做法
將依附元件和執行階段擷取到獨立檔案
依附元件
應用程式依附元件的變更頻率通常低於實際的應用程式程式碼。如果遷移 並將這些內容儲存在獨立的檔案中,瀏覽器可以分別快取這些檔案。 而且不會每次應用程式程式碼變更時重新下載這些項目。
如要將依附元件擷取至不同的區塊,請執行三個步驟:
將輸出檔案名稱替換為
[name].[chunkname].js
:// webpack.config.js module.exports = { output: { // Before filename: 'bundle.[chunkhash].js', // After filename: '[name].[chunkhash].js' } };
Webpack 建構應用程式時會取代
[name]
並以區塊名稱命名如未新增[name]
部分,則 以雜湊的方式區分區塊,這是相當困難的!將
entry
欄位轉換為物件:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
在這個程式碼片段中,「main」是區塊名稱這個名稱會取代 步驟 1 中的
[name]
地點。現在,如果您建構應用程式,這個區塊會包含完整的應用程式程式碼, 但我們尚未完成這些步驟但這項變更會在幾秒鐘內變更。
在 webpack 4 中新增
optimization.splitChunks.chunks: 'all'
選項 在您的 webpack 設定中:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
這個選項會啟用智慧型程式碼分割功能。然後,webpack 會擷取供應商程式碼 大小就會大於 30 KB (壓縮前和使用 gzip 前)。它也會擷取常見的程式碼 如果您的建構會產生多個套件 (例如 若您將應用程式分割為路徑)。
在 webpack 3 中新增
CommonsChunkPlugin
:// webpack.config.js (for webpack 3) module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ // A name of the chunk that will include the dependencies. // This name is substituted in place of [name] from step 1 name: 'vendor', // A function that determines which modules to include into this chunk minChunks: module => module.context && module.context.includes('node_modules'), }) ] };
這個外掛程式會採用所有路徑包含
node_modules
和 會將這些內容移至另一個名為vendor.[chunkhash].js
的檔案。
完成這些變更後,每個版本都會產生兩個檔案,而不是一個:main.[chunkhash].js
和
vendor.[chunkhash].js
(Webpack 4 的 vendors~main.[chunkhash].js
)。以 webpack 4 來說
如果依附元件較小,系統可能無法產生供應商套裝組合,但沒關係:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
瀏覽器會分別快取這些檔案,並且只重新下載變更的程式碼。
Webpack 執行階段程式碼
很抱歉,僅擷取供應商程式碼是不夠的。如果您嘗試 請修改應用程式的程式碼:
// index.js
…
…
// E.g. add this:
console.log('Wat');
您會發現,vendor
雜湊也會變更:
Asset Size Chunks Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
↓。
Asset Size Chunks Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor
會發生這種情況,是因為 webpack 組合與模組程式碼不同,具有 Runtime – 一小段程式碼 這個架構會用於管理模組執行作業將程式碼分成多個檔案後 這段程式碼會在區塊 ID 與 對應檔案:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack 包含這個執行階段至最後一個產生的區塊,也就是 vendor
讓我們舉例說明一下每當有區塊變更時,這段程式碼也會跟著改變
導致整個 vendor
區塊改變。
為解決這個問題,請將執行階段移至個別檔案。在 webpack 4 中
方法是啟用 optimization.runtimeChunk
選項:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
在 webpack 3 中,請使用 CommonsChunkPlugin
建立額外的空白區塊:
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: module => module.context && module.context.includes('node_modules')
}),
// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity
})
]
};
變更完成後,每個版本都會產生三個檔案:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
按照反向順序將這些內容加入 index.html
中,這樣就大功告成了:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
延伸閱讀
- Webpack 指南:長期快取
- Webpack 說明文件:關於 webpack 執行階段, 資訊清單
- "充分運用 CommonsChunkPlugin"
optimization.splitChunks
和optimization.runtimeChunk
的運作方式
內嵌 webpack 執行階段,以儲存額外的 HTTP 要求
如要進一步改善,請嘗試將 webpack 執行階段內嵌至 HTML 回應。意即,而不是:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
:
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
執行階段相當小,內嵌這項功能有助於儲存 HTTP 要求 (美化 與 HTTP/1 之間的重要連線使用 HTTP/2 時較不重要,但仍可能在 效果)。
做法如下。
如果您使用 HtmlWebpackPlugin 產生 HTML
如果您使用 HtmlWebpackPlugin: HTML 檔案 InlineSourcePlugin 您只需要:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
inlineSource: 'runtime~.+\\.js',
}),
new InlineSourcePlugin()
]
};
如果您使用自訂伺服器邏輯來產生 HTML
使用 webpack 4:
將
WebpackManifestPlugin
敬上 即可得知執行階段區塊的生成名稱:// webpack.config.js (for webpack 4) const ManifestPlugin = require('webpack-manifest-plugin'); module.exports = { plugins: [ new ManifestPlugin() ] };
使用此外掛程式的建構作業會建立如下的檔案:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }
內嵌執行階段區塊的內容既輕鬆又方便。例如:Node.js 和 Express:
// server.js const fs = require('fs'); const manifest = require('./manifest.json'); const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
或者使用 webpack 3:
請指定
filename
,將執行階段名稱設為靜態:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
輕鬆內嵌
runtime.js
內容。例如:Node.js 和 Express:// server.js const fs = require('fs'); const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
目前不需要的延遲載入程式碼
有時候,網頁中的重要部分會比較少:
- 當您在 YouTube 上載入影片網頁時,您重視的是影片,而不是 留言。而影片的重要性更勝於留言
- 在新聞網站上開啟文章時,您會比較重視 而不是廣告因此,文字比廣告更重要。
在這種情況下,建議您只下載
然後延遲載入其餘部分使用
import()
函式和
導入code-splitting功能:
// videoPlayer.js
export function renderVideoPlayer() { … }
// comments.js
export function renderComments() { … }
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
import()
會指定您想要動態載入特定模組。時間
webpack 看到了 import('./module.js')
,這個模組會將這個模組移到獨立的
chunk:
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
並只在執行作業達到 import()
函式時下載。
這麼做會縮小 main
套件,進而縮短初始載入時間。
更棒的是,如果變更主區塊中的程式碼,也可以改善快取品質。
註解區塊不會受到影響
延伸閱讀
- 「
import()
」的 Webpack 說明文件 函式 - 用於導入
import()
的 JavaScript 提案 語法
將程式碼分割為路徑和頁面
如果您的應用程式有多個路徑或頁面,但只有一個 JS 檔案
(單一 main
區塊),很可能會在基礎上提供額外的位元組
每個要求舉例來說,假設使用者造訪您網站的首頁:
![WebFundamentals 首頁](https://web.developers.google.cn/static/articles/use-long-term-caching/image/a-webfundamentals-home-pa-086040f923b7f.png?authuser=19&hl=zh-tw)
就不需要載入程式碼來轉譯位於另一部 但他們會載入網頁除此之外,如果使用者總是只到住家 但更改文章程式碼後,Webpack 就會使 整個套件 – 而且使用者必須重新下載整個應用程式。
如果我們將應用程式分成多個頁面 (如果是單頁應用程式,則為路徑) 只會下載相關程式碼此外,瀏覽器會快取應用程式的程式碼 如果您更改首頁程式碼,Webpack 只會導致 對應的區塊
單頁應用程式
如要按照路徑分割單頁應用程式,請使用 import()
(請參閱
」部分)。如果您使用架構
可能已有以下解決方案:
- "程式碼
分割中"
在
react-router
文件中撰寫 (針對 React) - 「延遲載入
路徑」部分
vue-router
的文件 (適用於 Vue.js)
傳統多頁面應用程式
如要將傳統應用程式依頁面分割,請使用 webpack 的 項目 積分。如果您的應用程式有三個 包括首頁、文章網頁和使用者帳戶頁面等等 應該會有三個項目:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
}
};
Webpack 會為每個項目檔案建構個別的依附元件樹狀結構 僅包含該項目使用的模組的套件:
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
因此,如果只有文章網頁使用 Lodash,home
和 profile
組合
「不包含」這個程式庫,使用者也不必下載這個程式庫
造訪首頁
不過,不同的依附元件樹狀結構會有缺點。如果有兩個進入點,請使用
Lodash,且您尚未將依附元件移至供應商套裝組合 (
點數會包含 Lodash 的副本如要解決這個問題,請在 webpack 4 中新增
將 optimization.splitChunks.chunks: 'all'
選項新增至 webpack 設定中:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
這個選項會啟用智慧型程式碼分割功能。如採用這個選項,Webpack 會自動 找出通用程式碼,然後解壓縮成個別檔案
或者,在 webpack 3 中,請使用 CommonsChunkPlugin
,它會將通用的依附元件移至新的指定檔案:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
您可以嘗試使用 minChunks
值來找出最適合的變數。一般而言
需要保持小一點
但如果區塊數變大適用對象
舉例來說,3 個區塊的 minChunks
可能是 2 個,但以 30 個區塊來說,可能是 8 個
- 既然模型維持在 2,將太多模組放入通用檔案中
過度加載它
延伸閱讀
- Webpack 說明文件:有關項目概念的 Webpack 文件 分數
- 關於 CommonsChunkPlugin
- "充分運用 CommonsChunkPlugin"
optimization.splitChunks
和optimization.runtimeChunk
的運作方式
讓模組 ID 更穩定
建構程式碼時,Webpack 會為每個模組指派 ID。這些 ID
用於套件中的 require()
中。您通常會在建構輸出內容中看到 ID
放在模組路徑前面:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
↓ 此處
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
根據預設,系統會使用計數器計算 ID (也就是說,第一個模組的 ID 是 0, 第二組 ID 是 1,以此類推)。但問題在於 新模組可能會出現在模組清單中間,改變所有 下一個單元ID:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
↓ 模組...
[4] ./webPlayer.js 24 kB {1} [built]
↓,快來看看這一切!comments.js
現在有 ID 5,而不是 4
[5] ./comments.js 58 kB {0} [built]
↓ ads.js
現在 ID 是 6,而不是 5
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
如此一來,包含或依附於含有變更 ID 的模組的所有區塊,都將失效。
即使實際程式碼維持不變在此範例中,0
區塊 (區塊
與 comments.js
) 和 main
區塊 (與其他應用程式程式碼的區塊) 取得
已失效,但只應輸入 main
。
如要解決這個問題,請使用
HashedModuleIdsPlugin
。
它會將計數器式 ID 替換為模組路徑的雜湊:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
↓ 此處
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
如果使用這個方法,只有在重新命名或移動該模組時,模組的 ID 才會變更 後續課程我們將逐一介紹 預先訓練的 API、AutoML 和自訂訓練新模組不會影響其他模組而非客戶 ID
如要啟用外掛程式,請將外掛程式新增至設定的 plugins
區段:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
延伸閱讀
總結
- 變更軟體包名稱,藉此快取套件並區分不同版本
- 將套件分割成應用程式的程式碼、供應商程式碼和執行階段
- 內嵌執行階段以儲存 HTTP 要求
- 使用
import
延遲載入非重要程式碼 - 依路徑/網頁分割程式碼,以免載入不必要的內容