有沒有必要上?帶你 Webpack5 快速開箱!
大家一定看過很多電子設(shè)備開箱測(cè)評(píng),今天我們也來跑一個(gè)軟件新版的上手測(cè)評(píng) —— Webpack 5!
從 2017 年發(fā)出關(guān)于 v5 的投票開始,到 2019 年 10 月發(fā)布第一個(gè) beta 版本,目前是 5.0.0-beta.16。現(xiàn)在在收集使用反饋、生態(tài)升級(jí)的過程中,相信不久后就可以正式發(fā)布了。這次升級(jí)重點(diǎn):性能改進(jìn)、Tree Shacking、Code Generation、Module Federation。
下面我們跟著 Changelog 來動(dòng)手,測(cè)測(cè)重點(diǎn)內(nèi)容~
優(yōu)化持久緩存
首先簡(jiǎn)單說 Webpack 中 graph 的概念:
Webpack 在執(zhí)行的時(shí)候,以配置的 entry 為入口,遞歸解析文件依賴,構(gòu)建一個(gè) graph,記錄代碼中各個(gè) module 之間的關(guān)系。每當(dāng)有文件更新的時(shí)候, 遞歸過程會(huì)重來,graph 發(fā)生改變。
如果簡(jiǎn)單粗暴地重建 graph 再編譯,會(huì)有很大的性能開銷。Webpack 利用緩存實(shí)現(xiàn)增量編譯,從而提升構(gòu)建性能。
緩存(內(nèi)存 / 磁盤兩種形式)中的主要內(nèi)容是 module objects,在編譯的時(shí)候會(huì)將 graph 以二進(jìn)制或者 json 文件存儲(chǔ)在硬盤上。每當(dāng)代碼變化、模塊之間依賴關(guān)系改變導(dǎo)致 graph 改變時(shí), Webpack 會(huì)讀取記錄做增量編譯。
之前可以使用 loader 設(shè)置緩存:
使用 cache-loader可以將編譯結(jié)果寫入硬盤緩存,Webpack 再次構(gòu)建時(shí)如果文件沒有發(fā)生變化則會(huì)直接拉取緩存還有一部分 loader 自帶緩存配置,比如 babel-loader,可以配置參數(shù)cacheDirectory使用緩存,將每次的編譯結(jié)果寫進(jìn)磁盤(默認(rèn)在 node_modules/.cache/babel-loader 目錄)
v5 中緩存默認(rèn)是 memory,你可以修改設(shè)置寫入硬盤:
module.exports = {cache: {type: 'filesystem',// cacheDirectory 默認(rèn)路徑是 node_modules/.cache/webpackcacheDirectory: path.resolve(__dirname, '.temp_cache')}};
注:對(duì)大部分 node_modules 哈希處理以構(gòu)建依賴項(xiàng),代價(jià)昂貴,還降低 Webpack 執(zhí)行速度。為避免這種情況出現(xiàn),Webpack 加入了一些優(yōu)化,默認(rèn)會(huì)跳過 node_modules,并使用 package.json 中的 version 和 name 作為數(shù)據(jù)源。
優(yōu)化長(zhǎng)期緩存
Webpack 5 針對(duì) moduleId ?和 chunkId 的計(jì)算方式進(jìn)行了優(yōu)化,增加確定性的 moduleId 和 chunkId 的生成策略。moduleId 根據(jù)上下文模塊路徑,chunkId 根據(jù) chunk 內(nèi)容計(jì)算,最后為 moduleId 和 chunkId 生成 3 - 4 位的數(shù)字 id,實(shí)現(xiàn)長(zhǎng)期緩存,生產(chǎn)環(huán)境下默認(rèn)開啟。
對(duì)比原來的 moduleId
原來的 moduleId 默認(rèn)值是自增 id,容易導(dǎo)致文件緩存失效。在 v4 之前,可以安裝 HashedModuleIdsPlugin 插件覆蓋默認(rèn)的 moduleId 規(guī)則, 它會(huì)使用模塊路徑生成的 hash 作為 moduleId。在 v4 中,可以配置 optimization.moduleIds = 'hashed'
對(duì)比原來的 chunkId
原來的 chunkId 默認(rèn)值自增 id。比如這樣的配置下,如果有新的 entry 增加,chunk 數(shù)量也會(huì)跟著增加,chunkId 也會(huì)遞增。之前可以安裝 NamedChunksPlugin 插件來穩(wěn)定 chunkId;或者配置 optimization.chunkIds = 'named'
NodeJS 的 polyfill 腳本被移除
最開始,Webpack 目標(biāo)是允許在瀏覽器中運(yùn)行 Node 模塊。但是現(xiàn)在在 Webpack 看來,大多模塊就是專門為前端開發(fā)的。在 v4 及以前的版本中,對(duì)于大多數(shù)的 Node 模塊會(huì)自動(dòng)添加 polyfill 腳本,polyfill 會(huì)加到最終的 bundle 中,其實(shí)通常情況下是沒有必要的。在 v5 中將停止這一行為。
比如以下一段代碼:
// index.jsimport sha256 from 'crypto-js/sha256';const hashDigest = sha256('hello world');console.log(hashDigest);
在 v4 中,會(huì)主動(dòng)添加 crypto 的 polyfill,也就是 crypto-browserify。我們運(yùn)行的代碼是不需要的,反而最后的包變大,編譯結(jié)果 「417 kb」:
在 v5 中,如果遇到了這樣的情況,會(huì)提示你進(jìn)行確認(rèn)。如果確認(rèn)不需要 node polyfill,按照提示 alias 設(shè)置為 false 即可。最后的編譯結(jié)果僅有 「5.69 kb」:
配置 resolve.alias: { crypto: false }:
瀏覽器執(zhí)行結(jié)果:
更好的 TreeShaking
現(xiàn)在有這樣一段代碼:
// inner.jsexport const a = 'aaaaaaaaaa';export const b = 'bbbbbbbbbb';// module.jsimport * as inner from "./inner";export { inner };// index.jsimport * as module from "./module";console.log(module.inner.a);
在 v4 中毫無(wú)疑問,以上代碼 a、b 變量是被全部打包的:
但我們只調(diào)用了 a 變量,理想情況應(yīng)該是 b 被識(shí)別為 unused,不被打包。這一優(yōu)化在 v5 中實(shí)現(xiàn)了。在 v5 中會(huì)分析模塊 export 與 import 之間的依賴關(guān)系,最終的代碼生成非常簡(jiǎn)潔:
重大的變革
如果說以上的變更優(yōu)化都是常規(guī)路數(shù),那么下面的功能有點(diǎn)出乎意料。
Module Federation
讓 Webpack 達(dá)到了線上 runtime 的效果,讓代碼直接在獨(dú)立應(yīng)用間利用 CDN 直接共享,不再需要本地安裝 NPM 包、構(gòu)建再發(fā)布了!
設(shè)計(jì)初衷
Webpack 認(rèn)同多個(gè)單獨(dú)的構(gòu)建應(yīng)能夠構(gòu)成一個(gè)應(yīng)用。這些獨(dú)立的構(gòu)建不相互依賴,因此可以單獨(dú)開發(fā)和部署。這通常稱為微型前端,但還不僅僅是如此。
在之前我們希望共享代碼是如何做的?
「NPM」
維護(hù)一個(gè) CommonComponents 的 NPM 包,在不同項(xiàng)目中安裝、使用。如果 NPM 包升級(jí),對(duì)應(yīng)項(xiàng)目都需要安裝新版本,本地編譯,打包到 bundle 中。
「UMD」
UMD 優(yōu)點(diǎn)在 runtime。缺點(diǎn)也明顯,體積優(yōu)化不方便,容易有版本沖突。
「微前端」
獨(dú)立應(yīng)用間的共享也是問題。一般有兩種打包方式:
子應(yīng)用獨(dú)立打包,模塊解耦了,但公共的依賴不易維護(hù)處理 整體應(yīng)用一起打包,能解決公共依賴;但龐大的多個(gè)項(xiàng)目又使打包變慢,后續(xù)也不好擴(kuò)展
Webpack 5 實(shí)現(xiàn)了全新的解決方案

從圖中可以看到,這個(gè)方案是直接將一個(gè)應(yīng)用的 bundle,應(yīng)用于另一個(gè)應(yīng)用。
應(yīng)用可以模塊化輸出,就是說它本身可以自我消費(fèi),也可以動(dòng)態(tài)分發(fā) runtime 子模塊給其他應(yīng)用。
理論比較抽象,我們動(dòng)手試一下。
實(shí)踐測(cè)試
現(xiàn)在有兩個(gè)應(yīng)用 app1 (localhost:3001)、app2 (localhost:3002):

入口文件:
// app1 & app2: index.jsimport App from "./App";import React from "react";import ReactDOM from "react-dom";ReactDOM.render(, document.getElementById("root"));
app2 生產(chǎn)了 Button 組件:
// app2: Button.jsimport React from "react";const Button = () => ;export default Button;
app2 自身消費(fèi) Button 組件:
// app2: App.jsimport LocalButton from "./Button";import React from "react";const App = () => (Basic Host-Remote
App 2
);export default App;
app1 引用 app2 的 Button 組件:
// app1: App.jsimport React from "react";const RemoteButton = React.lazy(() => import("app2/Button"));const App = () => (Basic Host-Remote
App 1
);export default App;
先看生產(chǎn)了 Button 組件的 app2,其配置文件:
// app2:webpack.config.jsconst HtmlWebpackPlugin = require("html-webpack-plugin");const { ModuleFederationPlugin } = require("webpack").container;const path = require("path");module.exports = {entry: "./src/index",mode: "development",devServer: {contentBase: path.join(__dirname, "dist"),port: 3002,},output: {publicPath: "http://localhost:3002/",},module: {rules: [// ...],},plugins: [new ModuleFederationPlugin({name: "app2Lib",library: { type: "var", name: "app2Lib" },filename: "app2-remote-entry.js",exposes: {Button: "./src/Button",},shared: ["react", "react-dom"],}),new HtmlWebpackPlugin({template: "./index.html",}),],};
這段配置描述了,需要暴露出 Button 組件、需要依賴 react、react-dom。管理 exposes 和 shared 的模塊為 app2Lib,生成入口文件名為 app-remote-entry.js。
app1 的配置文件:
const HtmlWebpackPlugin = require("html-webpack-plugin");const { ModuleFederationPlugin } = require("webpack").container;const path = require("path");module.exports = {entry: "./src/index",mode: "development",devServer: {contentBase: path.join(__dirname, "dist"),port: 3001,},output: {publicPath: "http://localhost:3001/",},module: {rules: [// ...],},plugins: [new ModuleFederationPlugin({name: "app1",library: { type: "var", name: "app1" },remotes: {app2: "app2Lib",},shared: ["react", "react-dom"],}),new HtmlWebpackPlugin({template: "./index.html",}),],};
這段配置描述了,使用遠(yuǎn)端模塊 app2Lib,依賴 react、react-dom。
最后一步:在 app1 html 中加載 app2-remote-entry.js:
// app1: index.html
運(yùn)行結(jié)果:


「引用的 app2/Button 是如何找到的呢?」
通過 app1 的配置文件,知道了 app2 是遠(yuǎn)端加載。在生成的 app1 main.js 描述為:

看這里的 data 數(shù)組:
data[1] 即 webpack/container/reference/app2,這里是返回 app2Lib 對(duì)象:
module.exports = app2Lib;data[0] 即 webpack/container/remote-overrides/a46c3e,這里提供了 app2 需要的 react、react-dom 依賴,并返回 app2Lib:
module.exports = (external) => {if (external.override) {external.override(Object.assign({"react": () => {return Promise.resolve().then(() => {return () => __webpack_require__(/*! react */ "./node_modules/react/index.js")})},"react-dom": () => {return Promise.resolve().then(() => {return () => __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js")})}}, __webpack_require__.O))}return external;};
所以最后 promise 的賦值變成了:
var promise = app2Lib.get('Button');這么一看,app2Lib 是全局變量呀。
繼續(xù)看 app1 加載的 app2-remote-entry.js 內(nèi)容。果然,生成了一個(gè)全局變量 app2Lib:

app2Lib 對(duì)象擁有兩個(gè)方法,具體為:

var get = (module) => {return (__webpack_require__.o(moduleMap, module)? moduleMap[module](): Promise.resolve().then(() => {throw new Error('Module \"' + module + '\" does not exist in container.');}));};var override = (override) => {Object.assign(__webpack_require__.O, override);};
所以,app2/Button 實(shí)際就是 app2Lib.get('Button'),然后根據(jù)映射找到模塊,隨后__webpack_require__:
var moduleMap = {"Button": () => {return __webpack_require__.e("src_Button_js").then(() =>() => __webpack_require__(/*! ./src/Button */ "./src/Button.js"));}};
最后再說 shared: ['react', 'react-dom']:
app2 中指明了需要依賴 react、react-dom,并期望消費(fèi)的應(yīng)用提供。如果 app1 沒有提供,或沒有提供指定版本,如下把代碼注釋:
plugins: [new ModuleFederationPlugin({name: "app1",library: { type: "var", name: "app1" },remotes: {'app2': "app2Lib",},// shared: ["react", "react-dom"],// 版本不一致同理// shared: {// "react-15": "react",// "react-dom": "react-dom",// },}),new HtmlWebpackPlugin({template: "./index.html",}),]
那么,剛才 app1 main.js 中的 data[0] 即 webpack/container/remote-overrides/a46c3e 會(huì)變?yōu)椋?/p>module.exports = (external) => { if (external.override) { external.override(__webpack_require__.O); // external.override(Object.assign({ // "react": () => { // return Promise.resolve().then(() => { // return () => __webpack_require__(/*! react */ "./node_modules/react/index.js") // }) // }, // "react-dom": () => { // return Promise.resolve().then(() => { // return () => __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js") // }) // } // }, __webpack_require__.O)) } return external;};
app1 則從 app2 加載 react 依賴:


總結(jié),根據(jù) app2 配置的 exposes & shared 內(nèi)容,產(chǎn)生對(duì)應(yīng)的模塊文件,以及模塊映射關(guān)系,通過全局變量 app2Lib 進(jìn)行訪問;app1 通過全局變量 get 能知道應(yīng)該去如何加載 button.js,override 能知道共享依賴的模塊。
以上,F(xiàn)ederation 初看很像 DLL + External,但好處是你無(wú)需手動(dòng)維護(hù)、打包依賴,代碼運(yùn)行時(shí)加載。這種模式下,調(diào)試也變得容易,不再需要復(fù)制粘貼代碼或者 npm link,只需要啟動(dòng)應(yīng)用即可。這里僅以 Button 組件為例,Button 可以一個(gè)組件,也可以是一個(gè)頁(yè)面、一個(gè)應(yīng)用。Module Federation 的落地,結(jié)合自動(dòng)化流程等系列工作,還需要大家在各自場(chǎng)景中實(shí)踐。
社區(qū)探索實(shí)踐
各種復(fù)雜場(chǎng)景樣例:https://github.com/module-federation/module-federation-examples/ 騰訊:探索 webpack5 新特性 Module federation 在騰訊文檔的應(yīng)用 螞蟻:調(diào)研 Federated Modules,應(yīng)用秒開,應(yīng)用集方案,微前端加載方案改進(jìn)等 百度:reskript webpack5 升級(jí)實(shí)驗(yàn)
其他特性
Top Level Await SplitChunks 支持更靈活的資源拆分 不包含 JS 代碼的 Chunk 將不再生成 JS 文件 Output 默認(rèn)生成 ES6 規(guī)范代碼,也支持配置為 5 - 11 ......
詳細(xì)請(qǐng)閱讀 Changlog
以上 Demo 官方也有給出,供大家參考。我們也將自己內(nèi)部項(xiàng)目做了升級(jí)嘗試,過程中會(huì)出現(xiàn)一些 plugins 不兼容的情況。根據(jù)官方 Changelog 說明,都可以找到答案,臨時(shí)修改下相關(guān) plugin 代碼。如果你的升級(jí)嘗試中也遇到了,可以自行處理下,同時(shí)也反饋回社區(qū),共同推進(jìn)新版發(fā)布進(jìn)程。
總的來說,Webpack 5 初步上手體驗(yàn)后,打包體積、速度都有不錯(cuò)的提升,多數(shù)功能的使用配置也更便捷靈巧,Module Federation 讓人眼前一亮。拋磚引玉,大家感興趣可以來交流各自的解讀和研究。
如果你對(duì)新鮮事物充滿好奇,喜歡專研技術(shù)、樂于分享,對(duì) ?IM 產(chǎn)品、桌面客戶端基礎(chǔ)引擎、基礎(chǔ)平臺(tái)建設(shè)感興趣,歡迎你的加入!
?文章作者:王欣瑜(Suite Commercialization Engineering 團(tuán)隊(duì))
?
字節(jié)跳動(dòng)飛書業(yè)務(wù),海量 hc,極速響應(yīng),快來和我成為同事吧~職位介紹
最后
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

