你們必定看過不少電子設備開箱測評,今天咱們也來跑一個軟件新版的上手測評 —— Webpack 5!javascript
從 2017 年發出關於 v5 的投票開始,到 2019 年 10 月發佈第一個 beta 版本,目前是 5.0.0-beta.16。如今在收集使用反饋、生態升級的過程當中,相信不久後就能夠正式發佈了。此次升級重點:性能改進、Tree Shacking、Code Generation、Module Federation。html
下面咱們跟着 Changelog 來動手,測測重點內容~前端
優化持久緩存
首先簡單說 Webpack 中 graph 的概念:java
Webpack 在執行的時候,以配置的 entry 爲入口,遞歸解析文件依賴,構建一個 graph,記錄代碼中各個 module 之間的關係。每當有文件更新的時候, 遞歸過程會重來,graph 發生改變。node
若是簡單粗暴地重建 graph 再編譯,會有很大的性能開銷。Webpack 利用緩存實現增量編譯,從而提高構建性能。react
緩存(內存 / 磁盤兩種形式)中的主要內容是 module objects,在編譯的時候會將 graph 以二進制或者 json 文件存儲在硬盤上。每當代碼變化、模塊之間依賴關係改變致使 graph 改變時, Webpack 會讀取記錄作增量編譯。webpack
以前可使用 loader 設置緩存:git
-
使用 cache-loader
能夠將編譯結果寫入硬盤緩存,Webpack 再次構建時若是文件沒有發生變化則會直接拉取緩存 -
還有一部分 loader 自帶緩存配置,好比 babel-loader
,能夠配置參數cacheDirectory
使用緩存,將每次的編譯結果寫進磁盤(默認在 node_modules/.cache/babel-loader 目錄)
v5 中緩存默認是 memory
,你能夠修改設置寫入硬盤:github
module.exports = { cache: { type: 'filesystem', // cacheDirectory 默認路徑是 node_modules/.cache/webpack cacheDirectory: path.resolve(__dirname, '.temp_cache') }};
注:對大部分 node_modules
哈希處理以構建依賴項,代價昂貴,還下降 Webpack 執行速度。爲避免這種狀況出現,Webpack 加入了一些優化,默認會跳過 node_modules
,並使用 package.json
中的 version
和 name
做爲數據源。web
優化長期緩存
Webpack 5 針對 moduleId
和 chunkId
的計算方式進行了優化,增長肯定性的 moduleId 和 chunkId 的生成策略。moduleId 根據上下文模塊路徑,chunkId 根據 chunk 內容計算,最後爲 moduleId 和 chunkId 生成 3 - 4 位的數字 id,實現長期緩存,生產環境下默認開啓。
-
對比原來的 moduleId
原來的 moduleId 默認值是自增 id,容易致使文件緩存失效。在 v4 以前,能夠安裝 HashedModuleIdsPlugin
插件覆蓋默認的 moduleId 規則, 它會使用模塊路徑生成的 hash 做爲 moduleId。在 v4 中,能夠配置 optimization.moduleIds = 'hashed'
-
對比原來的 chunkId
原來的 chunkId 默認值自增 id。好比這樣的配置下,若是有新的 entry 增長,chunk 數量也會跟着增長,chunkId 也會遞增。以前能夠安裝 NamedChunksPlugin
插件來穩定 chunkId;或者配置 optimization.chunkIds = 'named'
NodeJS 的 polyfill 腳本被移除
最開始,Webpack 目標是容許在瀏覽器中運行 Node 模塊。可是如今在 Webpack 看來,大多模塊就是專門爲前端開發的。在 v4 及之前的版本中,對於大多數的 Node 模塊會自動添加 polyfill 腳本,polyfill 會加到最終的 bundle 中,其實一般狀況下是沒有必要的。在 v5 中將中止這一行爲。
好比如下一段代碼:
// index.jsimport sha256 from 'crypto-js/sha256'; const hashDigest = sha256('hello world');console.log(hashDigest);
在 v4 中,會主動添加 crypto
的 polyfill,也就是 crypto-browserify
。咱們運行的代碼是不須要的,反而最後的包變大,編譯結果 「417 kb」:
在 v5 中,若是遇到了這樣的狀況,會提示你進行確認。若是確認不須要 node polyfill,按照提示 alias 設置爲 false 便可。最後的編譯結果僅有 「5.69 kb」:
配置 resolve.alias: { crypto: false }
:
瀏覽器執行結果:
更好的 TreeShaking
如今有這樣一段代碼:
// 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 中毫無疑問,以上代碼 a、b 變量是被所有打包的:
但咱們只調用了 a
變量,理想狀況應該是 b
被識別爲 unused
,不被打包。這一優化在 v5 中實現了。在 v5 中會分析模塊 export
與 import
之間的依賴關係,最終的代碼生成很是簡潔:
重大的變革
若是說以上的變動優化都是常規路數,那麼下面的功能有點出乎意料。
Module Federation
讓 Webpack 達到了線上 runtime 的效果,讓代碼直接在獨立應用間利用 CDN 直接共享,再也不須要本地安裝 NPM 包、構建再發布了!
設計初衷
Webpack 認同多個單獨的構建應可以構成一個應用。這些獨立的構建不相互依賴,所以能夠單獨開發和部署。這一般稱爲微型前端,但還不只僅是如此。
在以前咱們但願共享代碼是如何作的?
「NPM」
維護一個 CommonComponents 的 NPM 包,在不一樣項目中安裝、使用。若是 NPM 包升級,對應項目都須要安裝新版本,本地編譯,打包到 bundle 中。
「UMD」
UMD 優勢在 runtime。缺點也明顯,體積優化不方便,容易有版本衝突。
「微前端」
獨立應用間的共享也是問題。通常有兩種打包方式:
-
子應用獨立打包,模塊解耦了,但公共的依賴不易維護處理 -
總體應用一塊兒打包,能解決公共依賴;但龐大的多個項目又使打包變慢,後續也很差擴展
Webpack 5 實現了全新的解決方案
從圖中能夠看到,這個方案是直接將一個應用的 bundle,應用於另外一個應用。
應用能夠模塊化輸出,就是說它自己能夠自我消費,也能夠動態分發 runtime 子模塊給其餘應用。
理論比較抽象,咱們動手試一下。
實踐測試
如今有兩個應用 app1
(localhost:3001)、app2
(localhost:3002):
入口文件:
// app1 & app2: index.jsimport App from "./App";import React from "react";import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
app2
生產了 Button
組件:
// app2: Button.jsimport React from "react";
const Button = () => <button>App 2 Button</button>;
export default Button;
app2
自身消費 Button
組件:
// app2: App.jsimport LocalButton from "./Button";import React from "react";
const App = () => ( <div> <h1>Basic Host-Remote</h1> <h2>App 2</h2> <LocalButton /> </div>);
export default App;
app1
引用 app2
的 Button
組件:
// app1: App.jsimport 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;
先看生產了 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", }), ],};
這段配置描述了,使用遠端模塊 app2Lib
,依賴 react
、react-dom
。
最後一步:在 app1
html 中加載 app2-remote-entry.js
:
// app1: index.html<html> <head> <script src="http://localhost:3002/app2-remote-entry.js"></script> </head> <body> <div id="root"></div> </body></html>
運行結果:
「引用的 app2/Button
是如何找到的呢?」
經過 app1
的配置文件,知道了 app2
是遠端加載。在生成的 app1 main.js
描述爲:
看這裏的 data
數組:
data[1]
即 webpack/container/reference/app2
,這裏是返回 app2Lib
對象:
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
是全局變量呀。
繼續看 app1
加載的 app2-remote-entry.js
內容。果真,生成了一個全局變量 app2Lib
:
app2Lib
對象擁有兩個方法,具體爲:
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
實際就是 app2Lib.get('Button')
,而後根據映射找到模塊,隨後__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
,並指望消費的應用提供。若是 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
會變爲:
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
依賴:
總結,根據 app2
配置的 exposes
& shared
內容,產生對應的模塊文件,以及模塊映射關係,經過全局變量 app2Lib
進行訪問;app1
經過全局變量 get
能知道應該去如何加載 button.js
,override
能知道共享依賴的模塊。
以上,Federation 初看很像 DLL + External,但好處是你無需手動維護、打包依賴,代碼運行時加載。這種模式下,調試也變得容易,再也不須要複製粘貼代碼或者 npm link
,只須要啓動應用便可。這裏僅以 Button
組件爲例,Button
能夠一個組件,也能夠是一個頁面、一個應用。Module Federation 的落地,結合自動化流程等系列工做,還須要你們在各自場景中實踐。
社區探索實踐
-
各類複雜場景樣例:https://github.com/module-federation/module-federation-examples/ -
騰訊:探索 webpack5 新特性 Module federation 在騰訊文檔的應用 -
螞蟻: 調研 Federated Modules,應用秒開,應用集方案,微前端加載方案改進等 -
百度:reskript webpack5 升級實驗
其餘特性
-
Top Level Await -
SplitChunks 支持更靈活的資源拆分 -
不包含 JS 代碼的 Chunk 將再也不生成 JS 文件 -
Output 默認生成 ES6 規範代碼,也支持配置爲 5 - 11 -
......
詳細請閱讀 Changlog
以上 Demo 官方也有給出,供你們參考。咱們也將本身內部項目作了升級嘗試,過程當中會出現一些 plugins 不兼容的狀況。根據官方 Changelog 說明,均可以找到答案,臨時修改下相關 plugin 代碼。若是你的升級嘗試中也遇到了,能夠自行處理下,同時也反饋回社區,共同推動新版發佈進程。
總的來講,Webpack 5 初步上手體驗後,打包體積、速度都有不錯的提高,多數功能的使用配置也更便捷靈巧,Module Federation 讓人眼前一亮。拋磚引玉,你們感興趣能夠來交流各自的解讀和研究。
若是你對新鮮事物充滿好奇,喜歡專研技術、樂於分享,對 IM 產品、桌面客戶端基礎引擎、基礎平臺建設感興趣,歡迎你的加入!
❝文章做者:王欣瑜(Suite Commercialization Engineering 團隊)
❞
字節跳動飛書業務,海量 hc,極速響應,快來和我成爲同事吧~職位介紹
最後
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進技術羣,長期交流學習...
關注公衆號「前端下午茶」,持續爲你推送精選好文,也能夠加我爲好友,隨時聊騷。
本文分享自微信公衆號 - 前端下午茶(qianduanxiawucha)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。