有沒有必要上?帶你 Webpack5 快速開箱!


你們必定看過不少電子設備開箱測評,今天咱們也來跑一個軟件新版的上手測評 —— 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

  1. 使用 cache-loader 能夠將編譯結果寫入硬盤緩存,Webpack 再次構建時若是文件沒有發生變化則會直接拉取緩存
  2. 還有一部分 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 中的 versionname 做爲數據源。web

優化長期緩存

Webpack 5 針對 moduleId  和 chunkId 的計算方式進行了優化,增長肯定性的 moduleId 和 chunkId 的生成策略。moduleId 根據上下文模塊路徑,chunkId 根據 chunk 內容計算,最後爲 moduleId 和 chunkId 生成 3 - 4 位的數字 id,實現長期緩存,生產環境下默認開啓。

  1. 對比原來的 moduleId

原來的 moduleId 默認值是自增 id,容易致使文件緩存失效。在 v4 以前,能夠安裝 HashedModuleIdsPlugin 插件覆蓋默認的 moduleId 規則, 它會使用模塊路徑生成的 hash 做爲 moduleId。在 v4 中,能夠配置 optimization.moduleIds = 'hashed'

  1. 對比原來的 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 中會分析模塊 exportimport 之間的依賴關係,最終的代碼生成很是簡潔:

重大的變革

若是說以上的變動優化都是常規路數,那麼下面的功能有點出乎意料。

Module Federation

讓 Webpack 達到了線上 runtime 的效果,讓代碼直接在獨立應用間利用 CDN 直接共享,再也不須要本地安裝 NPM 包、構建再發布了!

設計初衷

Webpack 認同多個單獨的構建應可以構成一個應用。這些獨立的構建不相互依賴,所以能夠單獨開發和部署。這一般稱爲微型前端,但還不只僅是如此。

在以前咱們但願共享代碼是如何作的?

「NPM」

維護一個 CommonComponents 的 NPM 包,在不一樣項目中安裝、使用。若是 NPM 包升級,對應項目都須要安裝新版本,本地編譯,打包到 bundle 中。

「UMD」

UMD 優勢在 runtime。缺點也明顯,體積優化不方便,容易有版本衝突。

「微前端」

獨立應用間的共享也是問題。通常有兩種打包方式:

  1. 子應用獨立打包,模塊解耦了,但公共的依賴不易維護處理
  2. 總體應用一塊兒打包,能解決公共依賴;但龐大的多個項目又使打包變慢,後續也很差擴展

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 引用 app2Button 組件:

// 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 組件、須要依賴 reactreact-dom。管理 exposesshared 的模塊爲 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,依賴 reactreact-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 須要的 reactreact-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 中指明瞭須要依賴 reactreact-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.jsoverride 能知道共享依賴的模塊。

以上,Federation 初看很像 DLL + External,但好處是你無需手動維護、打包依賴,代碼運行時加載。這種模式下,調試也變得容易,再也不須要複製粘貼代碼或者 npm link,只須要啓動應用便可。這裏僅以 Button 組件爲例,Button 能夠一個組件,也能夠是一個頁面、一個應用。Module Federation 的落地,結合自動化流程等系列工做,還須要你們在各自場景中實踐。

社區探索實踐

其餘特性

  • 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,極速響應,快來和我成爲同事吧~職位介紹

最後



若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 歡迎加我微信「qianyu443033099」拉你進技術羣,長期交流學習...

  3. 關注公衆號「前端下午茶」,持續爲你推送精選好文,也能夠加我爲好友,隨時聊騷。

點個在看支持我吧,轉發就更好了


本文分享自微信公衆號 - 前端下午茶(qianduanxiawucha)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索