你們必定看過不少電子設備開箱測評,今天咱們也來跑一個軟件新版的上手測評 —— Webpack 5!javascript
從 2017 年發出關於 v5 的投票開始,到 2019 年 10 月發佈第一個 beta 版本,目前是 5.0.0-beta.16。如今在收集使用反饋、生態升級的過程當中,相信不久後就能夠正式發佈了。此次升級重點: 性能改進、Tree Shaking、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 再次構建時若是文件沒有發生變化則會直接拉取緩存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 默認值是自增 id,容易致使文件緩存失效。 在 v4 以前,能夠安裝 HashedModuleIdsPlugin
插件覆蓋默認的 moduleId 規則, 它會使用模塊路徑生成的 hash 做爲 moduleId。 在 v4 中,能夠配置 optimization.moduleIds = 'hashed'
原來的 chunkId 默認值自增 id。 好比這樣的配置下,若是有新的 entry 增長,chunk 數量也會跟着增長,chunkId 也會遞增。 以前能夠安裝 NamedChunksPlugin
插件來穩定 chunkId;或者配置 optimization.chunkIds = 'named'
最開始,Webpack 目標是容許在瀏覽器中運行 Node 模塊。可是如今在 Webpack 看來,大多模塊就是專門爲前端開發的。在 v4 及之前的版本中,對於大多數的 Node 模塊會自動添加 polyfill 腳本,polyfill 會加到最終的 bundle 中,其實一般狀況下是沒有必要的。在 v5 中將中止這一行爲。
好比如下一段代碼:
// index.js
import 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 }
:
瀏覽器執行結果:
如今有這樣一段代碼:
// inner.js
export const a = 'aaaaaaaaaa';
export const b = 'bbbbbbbbbb';
// module.js
import * as inner from "./inner";
export { inner };
// index.js
import * as module from "./module";
console.log(module.inner.a);
複製代碼
在 v4 中毫無疑問,以上代碼 a、b 變量是被所有打包的:
但咱們只調用了 a
變量,理想狀況應該是 b
被識別爲 unused
,不被打包。這一優化在 v5 中實現了。 在 v5 中會分析模塊 export
與 import
之間的依賴關係,最終的代碼生成很是簡潔:
若是說以上的變動優化都是常規路數,那麼下面的功能有點出乎意料。
讓 Webpack 達到了線上 runtime 的效果,讓代碼直接在獨立應用間利用 CDN 直接共享,再也不須要本地安裝 NPM 包、構建再發布了!
Webpack 認同多個單獨的構建應可以構成一個應用。 這些獨立的構建不相互依賴,所以能夠單獨開發和部署。 這一般稱爲微型前端,但還不只僅是如此。
NPM
維護一個 CommonComponents 的 NPM 包,在不一樣項目中安裝、使用。若是 NPM 包升級,對應項目都須要安裝新版本,本地編譯,打包到 bundle 中。
UMD
UMD 優勢在 runtime。缺點也明顯,體積優化不方便,容易有版本衝突。
微前端
獨立應用間的共享也是問題。通常有兩種打包方式:
從圖中能夠看到,這個方案是直接將一個應用的 bundle,應用於另外一個應用。
應用能夠模塊化輸出,就是說它自己能夠自我消費,也能夠動態分發 runtime 子模塊給其餘應用。
理論比較抽象,咱們動手試一下。
如今有兩個應用 app1
(localhost:3001)、app2
(localhost:3002):
入口文件:
// app1 & app2: index.js
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root")); 複製代碼
app2
生產了 Button
組件:
// app2: Button.js
import React from "react";
const Button = () => <button>App 2 Button</button>;
export default Button;
複製代碼
app2
自身消費 Button
組件:
// app2: App.js
import 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.js
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; 複製代碼
先看生產了 Button
組件的 app2
,其配置文件:
// app2: webpack.config.js
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: 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 的落地,結合自動化流程等系列工做,還須要你們在各自場景中實踐。
詳細請閱讀 Changlog
以上 Demo 官方也有給出,供你們參考。筆者也將本身內部項目作了升級測試,過程當中會出現一些 plugins 不兼容的狀況。根據官方 Changelog 說明,均可以找到答案,臨時修改下相關 plugin 代碼。若是你的升級嘗試中也遇到了,能夠自行處理下,同時也反饋回社區,共同推動新版發佈進程。
總的來講,Webpack 5 初步上手體驗後,打包體積、速度都有不錯的提高,多數功能的使用配置也更便捷靈巧,Module Federation 讓人眼前一亮。拋磚引玉,你們感興趣能夠來交流各自的解讀和研究。
若是你對新鮮事物充滿好奇,喜歡專研技術、樂於分享,對 IM 產品、桌面客戶端基礎引擎、基礎平臺建設感興趣,歡迎你的加入!
文章做者:王欣瑜
字節跳動飛書業務,海量 hc,極速響應,快來和我成爲同事吧~職位介紹