| 導語 2020 年 10 月 10 日,webpack5 正式發佈,並帶來了諸多重大的變動,將會使前端的構建效率與質量大爲提高。其實如今各大博客網站已經有不少關於 webpack5 的文章,但真正經過業務實踐並得到第一手數據的並很少,因此今天就給你們介紹一下 webpack5 在企鵝輔導業務中的升級與實踐ad下面是企鵝輔導h5項目分別在 webpack4 和 webpack5 版本下的構建實測數據,測試環境爲個人 MacBook Pro 15 寸高配。css
在上表打包的結果基礎之上,修改項目中的代碼後,從新進行打包獲得以下結果:html
打包後文件的大小:前端
從上表的測試結果能夠看出,webpack5 構建性能相對於 webpack4 提高不少,但在打包完成的 bundle 大小上,與 v4 差距不大。由此能夠看出 webpack5 的新特性帶來了一些優化,下面結合這些新的特性來分析爲何可以作到這些優化。node
webpack5 新特性react
webpack5 的發佈帶來了不少新的特性,例如優化持久緩存、優化長期緩存、Node Polyfill 腳本的移除、更優的 tree-shaking 以及 Module Federation 等。下面針對這些新的特性做出分析。webpack
一、編譯緩存git
顧名思義,編譯緩存就是在首次編譯後把結果緩存起來,在後續編譯時複用緩存,從而達到加速編譯的效果。github
1.一、webpack4 緩存方案web
webpack4 及以前的版本自己是沒有持久化緩存的能力的,只能藉助其餘的插件或 loader 來實現,例如:npm
使用 cache-loader 來緩存編譯結果到硬盤,再次構建時在緩存的基礎上增量編譯長期緩存。
使用自帶緩存的 loader,如:babel-loader,能夠配置 cacheDirectory
來將 babel 編譯的結果緩存下來。
使用 hard-source-webpack-plugin 來爲模塊提供中間緩存。
以下圖所示,使用以上緩存方案的結果,默認存儲在 node_modules/.cache 目錄下:
1.二、webpack5 緩存方案
webpack5 統一了持久化緩存的方案,有效下降了配置的複雜性。另外因爲 webpack 提供了構建的 runtime,全部被 webpack 處理的模塊都能獲得有效的緩存,大大提升了緩存的覆蓋率,所以 webpack5 的持久化緩存方案將會比其餘第三方插件緩存性能要好不少。
webpack5 緩存的開啓能夠經過如下配置來實現:
module.exports = {
cache: {
// 將緩存類型設置爲文件系統
type: "filesystem",
buildDependencies: {
/* 將你的 config 添加爲 buildDependency,
以便在改變 config 時得到緩存無效*/
config: [__filename],
/* 若是有其餘的東西被構建依賴,
你能夠在這裏添加它們*/
/* 注意,webpack.config,
加載器和全部從你的配置中引用的模塊都會被自動添加*/
},
// 指定緩存的版本
version: '1.0'
}
}
複製代碼
以下圖所示,webpack5 默認將構建的緩存結果放在 node_modules/.cache 目錄下,能夠經過配置更改目錄:
注意事項:
cache 的屬性 type 會在開發模式下被默認設置成 memory,並且在生產模式中被禁用,因此若是想要在生產打包時使用緩存須要顯式的設置。
爲了防止緩存過於固定,致使更改構建配置無感知,依然使用舊的緩存,默認狀況下,每次修改構建配置文件都會致使從新開始緩存。固然也能夠本身主動設置 version 來控制緩存的更新。
更多緩存的配置能夠參考官方文檔:
二、長效緩存
長效緩存指的是能充分利用瀏覽器緩存,儘可能減小因爲模塊變動致使的構建文件 hash
值的改變,從而致使文件緩存失效。
2.一、webpack4 長效緩存方案
webpack4 及以前的版本 moduleId
和 chunkId
默認是自增的,更改模塊的數量,容易致使緩存的失效。
使用腳手架建立一個簡單的項目,構建結果以下:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<React.StrictMode>
<div />
</React.StrictMode>,
document.getElementById('root'));
複製代碼
註釋掉入口文件 test.js 裏引用的 css 文件,如上代碼,構建結果以下:
由上圖可知,僅僅改了其中一個文件,結果構建出來的全部 js 文件的 hash
值都變了,不利於瀏覽器進行長效緩存。v4 以前的解決辦法是使用 HashedModuleIdsPlugin
固定 moduleId
,它會使用模塊路徑生成的 hash
做爲 moduleId
;使用 NamedChunksPlugin
來固定 chunkId
。
其中 webpack4 中能夠根據以下配置來解決此問題:
optimization.moduleIds = 'hashed'
optimization.chunkIds = 'named'
複製代碼
2.二、webpack5 長效緩存方案
webpack5 增長了肯定的 moduleId
,chunkId
的支持,以下配置:
optimization.moduleIds = 'deterministic'
optimization.chunkIds = 'deterministic'
複製代碼
此配置在生產模式下是默認開啓的,它的做用是以肯定的方式爲 module
和 chunk
分配 3-5 位數字 id
,相比於 v4 版本的選項 hashed
,它會致使更小的文件 bundles。
因爲 moduleId
和 chunkId
肯定了,構建的文件的 hash 值也會肯定,有利於瀏覽器長效緩存。同時此配置有利於減小文件打包大小。
在開發模式下,建議使用:
optimization.moduleIds = 'named'
optimization.chunkIds = 'named'
複製代碼
此選項生產對調試更友好的可讀的 id
。
三、Node Polyfill 腳本被移除
webpack4 版本中附帶了大多數 Node.js 核心模塊的 polyfill,一旦前端使用了任何核心模塊,這些模塊就會自動應用,可是其實有些是沒必要要的。
webpack5 將不會自動爲 Node.js 模塊添加 polyfill,而是更專一的投入到前端模塊的兼容中。所以須要開發者手動添加合適的 polyfill。
import sha256 from 'crypto-js/sha256';
const hashDigest = sha256('hello world1');
console.log(hashDigest);
複製代碼
上面代碼在v4中打包結果以下:
使用 wepack4 打包,主動添加了crypto
的 polyfill,即 crypto-browserify
,打包大小爲 441k。在 wepack5 中打包這樣的代碼,構建會提示開發者進行確認是否須要 node polyfill,以下圖:
若是確認不須要 polyfill,可根據提示設置 fallback
,以下:
resolve: {
fallback: {
"crypto": false
}
}
複製代碼
打包結果爲:
打包後 js 文件小了 305k,去除掉項目不須要的 node polyfill,對於減少打包大小收益很可觀。
四、更優的 tree-shaking
// const.js
export const a = 'hello';
export const b = 'world';
// module.js
export * as module from './const';
// index.js
import * as main from './module';
console.log(main.module.a)
複製代碼
有如上的一段代碼,在 v4 構建中打包後的結果以下:
從上圖能夠看出,const.js 導出的 a,b 變量都被打包了,但實際上咱們只用到了 a,期待的是b 應該不被打包進去。
webpack5 對 tree-shaking 進行了優化,分析模塊的 export
和 import
的依賴關係,去掉未被使用的模塊,打包結果以下:
!function(){"use strict"; console.log("hello")}();
複製代碼
能夠看出代碼很是簡潔。
Module Federation 使得使 JavaScript 應用得以從另外一個 JavaScript 應用中動態地加載代碼 —— 同時共享依賴。至關於 webpack 提供了線上 runtime 的環境,多個應用利用 CDN 共享組件或應用,不須要本地安裝 npm 包再構建了,這就有點雲組件的概念了。
以 github 上的例子爲例,basic-host-remote
上圖是項目的目錄結構,能夠看出存在 2 個應用 app一、app2。其中 app1 使用了 app2 的代碼,那麼 app1 是如何引用 app2 的代碼呢?看下面的代碼:
// app1
import React from "react";
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (
<div>
<h1>Basic Host-Remote</h1>
<h2>App 1</h2>
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>);
export default App;
複製代碼
其中最重要的就是
const RemoteButton = React.lazy(() => import("app2/Button"));
複製代碼
直接在 app1 的項目中引用了 app2 項目的代碼。是如何作到的?咱們看下構建配置:
先看提供組件 Button 的 app2 的配置:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
module.exports = {
// 有刪減
plugins: [
new ModuleFederationPlugin({
name: "app2",
library: {
type: "var",
name: "app2"
},
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/Button",
},
shared: {
react: {
singleton: true
},
"react-dom": {
singleton: true
}
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
複製代碼
依賴共享主要是由插件 ModuleFederationPlugin
來提供的,由上面的配置能夠看出 app2 暴露出了 Button 組件,依賴 react、react-dom,生成入口文件爲 remoteEntru.js。下面再來看下 app1的配置:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
module.exports = {
//http://localhost:3002/remoteEntry.js
plugins: [
new ModuleFederationPlugin({
name: "app1",
remotes: {
app2: "app2@http://localhost:3002/remoteEntry.js",
},
shared: {
react: {
singleton: true
},
"react-dom": {
singleton: true
}
},
}),
],
};
複製代碼
結合以前 app2 的配置來看,app1 加載遠程的 app2 模塊,依賴 react、react-dom。
瀏覽器裏運行效果如圖:
Module Federation 還有不少的潛力能夠挖掘,例如能夠將咱們項目中經常使用的依賴包 react 全家桶等打成一個包,作成一個 runtime,開發環境和生產環境依賴一個 runtime,這樣能夠大大減小項目的大小,提升編譯速度。
一些更實用的用法須要咱們在實際使用中繼續探索,發揮 webpack5 更大的價值。
一、在 webpack4 中標記過時的功能都已經在 webpack5 移除了。
二、開發環境下默認使用可讀的名稱爲 module
命名,不須要使用以下語法:
import(/* webpackChunkName: "name" */ "module")
複製代碼
三、原生 worker 支持
......
本文針對 webpack5 的比較重要的特性進行了說明,具體的一些變動能夠去參考官方文檔。
升級踩坑
升級的過程比較枯燥,基本上就是調試、修改、繼續調試的過程,下面列出幾個比較典型的問題。
一、升級 webpack 及相關包的版本
這個過程是比較耗時的,須要將 webpack 的版本及相關 loader 和 plugin 的版本進行升級,現在 webpack5 已正式發佈,相關插件基本上都兼容了 webpack5,因此大部分問題都能經過升級包版本解決。
二、配置 webpack5 編譯緩存不生效
這個問題就比較坑了,腳手架建立一個簡單項目後,根據官網文檔配置 cache,啓動構建:
webpack --config webpack-dist.config.js
cache: {
type: 'filesystem'
}
複製代碼
結果構建是成功,可是相應的緩存卻一直沒有生成,其中構建提示以下:
提示說 webpack-dist.config.js 找不到,當時就很懵了,這個文件明明是存在的,並且配置緩存策略時,並無這個文件。查閱大量文檔以後開始翻看源碼,其中部分以下:
// webpack/lib/cache/PackFileCacheStrategy.js
if (newBuildDependencies.size > 0 || !this.buildSnapshot) {
if (reportProgress)
reportProgress(0.5, "resolve build dependencies");
this.logger.debug(`Capturing build dependencies... (${Array.from(newBuildDependencies).join(", ")})`);
promise = new Promise((resolve, reject) => {
this.logger.time("resolve build dependencies");
this.fileSystemInfo.resolveBuildDependencies(this.context,newBuildDependencies,)
...
複製代碼
打印 newBuildDependencies 獲得結果:
發現還真有這個文件,並且相比於其餘絕對路徑,這個相對路徑可能沒法找到。
繼續斷點調試,追溯這裏的 newBuildDependencies 的值,發現webpack-dist.config.js 這個文件是在 webpack-cli 裏寫入的,
const cacheDefaults = (finalConfig, parsedArgs) => {
// eslint-disable-next-line no-prototype-builtins
const hasCache = finalConfig.hasOwnProperty('cache');
let cacheConfig = {};
if (hasCache && parsedArgs.config) {
if (finalConfig.cache && finalConfig.cache.type === 'filesystem') {
cacheConfig.buildDependencies = {
config: parsedArgs.config,
};
}
console.log(3333, cacheConfig)
return {
cache: cacheConfig
};
}
return cacheConfig;
};
複製代碼
從這裏看出當配置持久緩存時,使用命令行自動的給 cache 加上 config 後面的參數。因爲找不到這個相對路徑,從而致使緩存邏輯執行報錯,緩存失敗。
個人解決辦法:
const path = require('path');
const exec = require('child_process').exec;
const config = path.resolve(__dirname, 'webpack-dist.config.js');
const cmdStr = `webpack --config ${config}`;
exec(cmdStr, function(err,stdout,stderr){
if(err) {
console.log('get weather api error:'+stderr);
} else {
console.log(stdout);
}
});
複製代碼
獲取 webpack-dist.config.js 的絕對路徑,傳給命令行,就能夠解決。可能還有更優雅的解決方法,後面繼續探索。
三、loader 配置參數修改
出現以下報錯時,表示 webpack5 不兼容之前的 webpack 的寫法了,須要按最新版的規則來修改:
{
test: /\.css$/,
loaders: ['css-loader'],
// 提取出css
}
loaders改成use
{
test: /\.css$/,
use: ['css-loader'],
// 提取出css
}
複製代碼
四、去掉 node polyfill
因爲 webpack5 會自動去掉 polyfill,所以會出現以下提示
解決辦法是按照提示修改,確認是否須要添加 polyfill
resolve: {
fallback: {
"domain": false
}
}
複製代碼
webpack5 正式發佈已經有一段時間了,總的來講:
構建性能大幅度提高,依賴核心代碼層面的持久緩存,覆蓋率更高,配置更簡單。
打包後的代碼體積減小。
默認支持瀏覽器長期緩存,下降配置門檻。
使人激動的新特性 Module Federation,蘊含極大的可能性。