【重學webpack系列——webpack5.0】
1-15節主要講webpack的使用,當然,建議結合《webpack學完這些就夠了》一起學習。
從16節開始,專攻webpack原理,隻有深入原理,才能學到webpack設計的精髓,進而将技術點運用到實際項目中。
可以點選上方專欄訂閱哦。
以下是本節正文:
1. webpack5有哪些新特性( 面試題
)
面試題
- 啟動指令
- 持久化緩存
- 資源子產品
- URIs
-
&moduleIds
的優化chunkIds
- 更智能的
tree shaking
- nodeJs的
腳本被移除polyfill
- 子產品聯邦
2. 新特性1——啟動指令
- webpack4啟動devServer,用的指令是
webpack-dev-server
- webpack5啟動devServer,用的指令是
webpack serve
3.新特性2——持久化緩存
webpack5相對于webpack4,後面幾次打包會比首次打包時間可能會快80%,因為webpack5中可以配置cache(預設值為false),配置了之後,會将緩存存放在cacheDirectory中,第二次編譯的時候會去讀取緩存。
- webpack會緩存生成的webpack子產品和chunk,來改善建構速度
- 緩存在webpack5中預設開啟,緩存預設是在記憶體裡,但可以對
進行設定cache
- webpack 追蹤了每個子產品的依賴,并建立了檔案系統快照。此快照會與真實檔案系統進行比較,當檢測到差異時,将觸發對應子產品的重新建構
module.exprots = {
...
cache: {
type: 'memory', // 'memory' | 'filesystem'
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack')
}
}
4.新特性3——資源子產品
- 資源子產品是一種子產品類型,它允許使用資源檔案(字型,圖示等)而無需配置額外 loader
-
=>raw-loader
導出資源的源代碼asset/source
-
=>file-loader
發送一個單獨的檔案并導出 URLasset/resource
-
=>url-loader
導出一個資源的 data URIasset/inline
- asset 在導出一個 data URI 和發送一個單獨的檔案之間自動選擇。之前通過使用
,并且配置資源體積限制實作。當然asset也可以進行配置,根據配置來生成。url-loader
- 資源子產品目前是實驗性API,在webpack的配置檔案中需要配置一下,啟動資源子產品這個實驗性API
experiments: { // 啟用實驗性支援
asset: true, // 支援asset
},
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
module:{
rules: [
{
test: /\.png$/,
type: 'asset/resource'
},
{
test: /\.ico$/,
type: 'asset/inline' // 會生成一個base64字元串,寫了asset/inline就固定是base64了,不會因為大小來決定是否生成base64還是檔案。如果要有大小門檻值,那麼type就寫成asset,然後去加一個配置,如下面.jpg的使用方法
},
{
test: /\.txt$/,
type: 'asset/source' // 相當于以前的raw-loader
},
{
test: /\.jpg$/,
type: 'asset', // 單純寫asset,那麼就可以進行配置,通過parser配置
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
},
]
},
experiments: { // 啟用實驗性支援
asset: true, // 支援asset
},
}
5.新特性4——URIs
- Webpack 5 支援在請求中處理協定
- 支援data 支援 Base64 或原始編碼,MimeType可以在module.rule中被映射到加載器和子產品類型
- 這是一個實驗性的 api
5.1 使用舉例
import data from “data:text/javascript, export default 'title'”;
console.log(data)
6.新特性5——moduleIds 和 chunkIds 的優化
- 首先:
- module:每一個檔案其實都可以稱一個 module
- chunk:webpack 打包最終生成的代碼塊,代碼塊會生成檔案(bundle),一般來說一個 bundle 對應一個 chunk
-
然後,對于 moduleIds 和 chunkIds 的類型有如下幾種:
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-vAQK6gzf-1629215686789)(./_image/2021-08-17/[email protected])]
- 其中,natural 其實就是 webpack5 以前生産環境模式下的預設模式,
- 其中,named 其實就是 webpack 以前和現在開發環境下的模式
- 其中,size 是根據子產品大小來生成數字 id,用的很少
- 最後,deterministic 是 webpack5 新增的模式,也是 webpack5 生産環境下預設的模式,代表根據檔案名稱生成短 hash。
- webpack5 要新增 deterministic 的原因(
):面試點
以前,不管是 natural 這類是按照順序來命名産生的檔案的,這就有一個問題,如果原先産出的檔案是 1 2 和 3,後來我 2 相關的代碼删掉了,這時候産出的 1 不變,但是 3 就變成 2 了,這樣我 3 這個檔案就不能利用緩存了。如果按照 webpack5 的話,去掉一個 2,産出的依舊是 1 和 3,那麼 1 和 3 都可以從緩存中去讀取,這樣就大大加快了打包速度。
7.新特性6——移除 Node.js 的 polyfill
- webpack5 以前,webpack 會包含 nodejs 核心子產品的 polyfill,這樣的話,比如安裝了一個
子產品,那麼就可以直接使用,因為 node 的crypto
會自動啟動。polyfill
- 現在,webpack5 移除了 nodejs 的 polyfill,無法再直接使用類似
的子產品了。如果你想要使用類似crypto
的 nodejs 核心子產品,那麼可以在 webpack 配置檔案的crypto
中配置resolve
,配置了就可以使用了。如果不需要引用,将其置為 false 就可以了fallback
- 開啟了 fallback,也就是用了 node 核心子產品的 polyfill,打封包件的體積會變大。
module.exports = {
...
resolve: {
fallback: {
"crypto": require.resolve("crypto-browserify"), // 如果不需要,那麼就直接改為 false 就可以了
"buffer": require.resolve("buffer"),
"stream":require.resolve("stream-browserify")
}
}
}
8.新特性7——tree-shaking 進行了更新
- tree-shaking 是用于打包時候剔除沒有用到的代碼,以達到減小體積,縮短 http 請求時間,起到一定效果的頁面優化。
- webpack5 以前,tree-shaking 比較簡單,主要是找
進來的變量是否在這個子產品内出現過,出現過則不剔除,不出現過則剔除。并且用于 esModule 中import
- webpack5,可以進行根據作用域之間的關系進行優化。比如:
- a.js 中到處了兩個方法 a 和 b,在 index.js 中引入了 a.js 到處的 a 方法,沒有引用 b 方法。那麼 webpack4 打包出來的結果包含了 index.js 和 a.js 的内容,包含了沒有用到的 b 方法。但是 webpack5 的 treeshaking,會進行作用域分析,打包結果隻有 index 和 a 檔案中的 a 方法,沒有用到的 b 方法是不會被打包進來的。
- 是以:webpack4 的 treeshaking 是關注 import 了某個庫的什麼子產品,那麼我就打包什麼;webpack5 更精細化,直接分析到哪些變量有效地用到了,那麼我就打包哪些變量。
- 所謂的“有效”隻的就是活代碼。而非死代碼,類似引用了但是沒有使用,這就是死代碼,是需要剔除的。
9. 子產品聯邦Module Federation
9.1子產品聯邦是什麼( 面試點
)
面試點
比如我們在開發兩個應用A和B,A應用需要引用B應用,假設這兩個應用是兩個人開發的,都處于開發階段,那麼這時候就可以通過webpack的子產品聯邦Module Federation,将B應用暴露出去,然後A應用引用B應用。這樣就不需要每次B應用build完了給A,直接可以同步開發。
使用子產品聯邦,每個應用塊都應該是一個獨立的建構,這些建構都将編譯成容器,容器可以被其他應用或容器使用,引用子產品的引用者成為
,一個被引用的容器成為
host
remote
。
有點類似微前端,其實微前端方案中确實也有子產品聯邦的方案。
然後面試官可能會繼續問子產品聯邦或者問微前端。
9.2 為什麼會有子產品聯邦
- 子產品聯邦主要是為了不同開發小組共同開發一個或多個應用。
- 場景舉例:
- 應用将被劃分為更小的應用塊,一個應用塊,可以是比如頭部導航或者側邊欄的前端元件,也可以是資料擷取邏輯的邏輯元件
- 每個應用快由不同組開發
- 應用或應用快共享其他應用塊或庫
- 場景舉例:
9.3 子產品聯邦核心概念
- 使用子產品聯邦,每個應用塊都應該是一個獨立的建構,這些建構都将編譯成容器
- 容器可以被其他應用或容器使用
- 引用子產品的引用者成為
,一個被引用的容器成為host
remote
-
暴露給remote
,host
則可以使用這些暴露的子產品,這些子產品被稱為host
子產品remote
-
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-BUMqX0Uv-1629442062265)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210818141807511.png)]
9.4 webpack5中子產品聯邦的使用配置
9.4.1基本使用:
- 前置條件:多個應用,假設有應用A和應用B,并且是A引用B
- 那麼這裡就有兩個概念:
- B是remote,需要暴露出去
- A是host,需要引用B
- 那麼這裡就有兩個概念:
- 第一步:在A和B的webpack中引入子產品聯邦的插件
- 第二步:在A和B的webpack中使用子產品聯邦插件
module.exports = { ... plugins: [ new ModuleFederationPlugin({ // 關鍵就是這裡的配置 }) ] }
- 這裡的配置有哪些?
字段 類型 含義 name string 必傳值,即輸出的子產品名,被遠端引用時路徑為 n a m e / {name}/ name/{expose} library object 聲明全局變量的方式,name為umd的name filename string 建構輸出的檔案名 remotes object 遠端引用的應用名及其别名的映射,使用時以key值作為name exposes object 被遠端引用時可暴露的資源路徑及其别名 shared object 與其他應用之間可以共享的第三方依賴,使你的代碼中不用重複加載同一份依賴
- 這裡的配置有哪些?
- 第三步:先看remote,即被暴露出去的B:
-
建構輸出的檔案名,也就是打包出來的檔案名filename: "remoteEntry.js"
-
name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為 n a m e / {name}/ name/{expose}name: "remote"
-
被遠端引用時可暴露的資源路徑及其别名exposes: {}
-
exposes的鍵值對,決定了暴露的内容
-
key為暴露的元件名稱,但是要寫成路徑,這個路徑是代表目前remote容器根路徑下的NewsList(相對于remote容器根路徑)
exposes: { // 被遠端引用時可暴露的資源路徑及其别名 "./NewsList": "./src/NewsList", // 這裡的key雖然說是暴露的元件,但是key還是要寫成路徑的形式。這個路徑的意思是代表目前remote容器根路徑下的NewsList(相對于remote容器根路徑) },
-
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { ... plugins: [ // 下面這個插件的作用就是,這個項目打包後會産出名字為${filename}的檔案,并且這個容器叫做${name},同時會向外提供${exposes}的key對應的那些元件,value是向外提供的元件在目前項目中是哪個元件 // 另外,這個remote打包的産物,并不會覆寫項目的output.filename。兩者是獨立的。output中配置的會打包出來成為項目的産出,跟我們普通的打包一樣,這個remote容器的檔案remoteEntry.js也會打包出來,是專門用來給别人使用的。 new ModuleFederationPlugin({ filename: "remoteEntry.js", // 建構輸出的檔案名,也就是打包出來的檔案名 name: "remote", // name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為${name}/${expose} exposes: { // 被遠端引用時可暴露的資源路徑及其别名 "./NewsList": "./src/NewsList", // 這裡的key雖然說是暴露的元件,但是key還是要寫成路徑的形式。這個路徑的意思是代表目前remote容器根路徑下的NewsList(相對于remote容器根路徑) }, }) ] }
-
- 第四步:再看host,即引用remote的應用A:
-
建構輸出的檔案名,也就是打包出來的檔案名,這個名字個remote一樣沒事的,因為服務不一樣filename: "remoteEntry.js"
-
name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為 n a m e / {name}/ name/{expose}name: "host"
-
遠端引用的應用名及其别名的映射remotes: {}
- key作為遠端應用的别名,用在使用遠端應用的時候
- value表示遠端應用(remote容器)的位址:
- 位址格式:遠端子產品的[email protected]位址
new ModuleFederationPlugin({ // 使用子產品聯邦插件并配置 filename: "remoteEntry.js", // 建構輸出的檔案名,也就是打包出來的檔案名,這個名字個remote一樣沒事的,因為服務不一樣 name: "host", // name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為${name}/${expose} remotes: { // 遠端引用的應用名及其别名的映射,使用時以key作為名字,也就是别名 remoteY:'[email protected]://localhost:3000/remoteEntry.js'// 遠端引用remote容器。注意:格式是`遠端子產品的[email protected]位址` }, })
-
- 第五步:在A應用(host)中引用遠端容器B(remote)
- import動态加載遠端元件,引用
,這個remoteY/NewsList
就是webpack配置的remotes中的key,也就是别名,remoteY
就是遠端那個元件配置的webpack中exposes下的key(/NewsList
)重點
- 傳回的就是一個元件RemoteNewsList,按照普通元件使用即可
// 引用遠端容器(元件),傳回的就是一個元件RemoteNewsList const RemoteNewsList = React.lazy(() => import('remoteY/NewsList')); // 動态加載遠端元件,引用'remoteY/NewsList',這個`remoteY`就是webpack配置的remotes中的key,也就是别名,`/NewsList`就是遠端那個元件配置的webpack中exposes下的key const App = () => ( <div> <h2>本地元件Sliders</h2> <Sliders /> <React.Suspense fallback={<div>加載中</div>}> <RemoteNewsList/> </React.Suspense> </div> );
- import動态加載遠端元件,引用
-
:注意點
- A項目引用B,那麼A是host,B是遠端容器
- B用exposes暴露元件,A中remotes引用元件
- 如果AB共享某個庫,那麼shared在A中配置,也就是說共享的内容是在host中配置的
- A項目引用B,那麼A是host,B是遠端容器
9.4.2特性配置:
-
:shared
- 當目前項目作為host的時候,引用遠端容器,遠端容器的
會複用目前項目的内容shared配置中的内容
plugins: [ ... new ModuleFederationPlugin({ filename: "remoteEntry.js", name: "host", remotes: { remoteX:'[email protected]://localhost:6886/remoteEntry.js' }, shared: { // 當目前項目作為host的時候,引用遠端容器,遠端容器的react和react-dom會複用目前項目的react和react-dom react: { singleton: true}, // 如果有一個容器已經引用了react了,那麼另外一個容器就會複用react 'react-dom': {singleton: true} // 如果有一個容器已經引用了react了,那麼另外一個容器就會複用react } }) ]
- 當目前項目作為host的時候,引用遠端容器,遠端容器的
-
:雙向依賴
- A應用可以引用B的子產品,B也可以引用A的子產品,子產品聯邦的依賴共享是可以雙向的
- 當A引用B時候,A就是host,B就是remote;當B引用A時候,B就是host,A就是remote
// A應用 plugins: [ new HtmlWebpackPlugin({ template:'./public/index.html' }), new ModuleFederationPlugin({ filename: "remoteEntry.js", name: "remote", + remotes: { + host: "[email protected]://localhost:8000/remoteEntry.js" + }, exposes: { "./NewsList": "./src/NewsList", }, shared:{ react: { singleton: true }, "react-dom": { singleton: true } } }) ] // B應用 plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }), new ModuleFederationPlugin({ filename: "remoteEntry.js", name: "host", remotes: { remote: "[email protected]://localhost:3000/remoteEntry.js" }, + exposes: { + "./Slides": "./src/Slides", + }, shared:{ react: { singleton: true }, "react-dom": { singleton: true } } }) ]
- A應用可以引用B的子產品,B也可以引用A的子產品,子產品聯邦的依賴共享是可以雙向的
-
:多個remote
- remote是一個對象,可以有多個鍵值對,每個key就是引用的子產品,無論這個子產品來源于哪個應用,隻要是被暴露出來的就可以
plugins: [ new ModuleFederationPlugin({ filename: "remoteEntry.js", name: "all", + remotes: { + remote: "[email protected]://localhost:3000/remoteEntry.js", + host: "[email protected]://localhost:8000/remoteEntry.js", + }, shared:{ react: { singleton: true }, "react-dom": { singleton: true } } }) ]
9.4.2 demo代碼:
- A應用
// webpack.config.js
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); // 需要引入子產品聯邦的插件
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
// publicPath: "http://localhost:3000/",
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
devServer: {
port: 3000
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
},
},
exclude: /node_modules/,
},
]
},
plugins: [
new HtmlWebpackPlugin({
template:'./public/index.html'
}),
// 下面這個插件的作用就是,這個項目打包後會産出名字為${filename}的檔案,并且這個容器叫做${name},同時會向外提供${exposes}的key對應的那些元件,value是向外提供的元件在目前項目中是哪個元件
// 另外,這個remote打包的産物,并不會覆寫項目的output.filename。兩者是獨立的。output中配置的會打包出來成為項目的産出,跟我們普通的打包一樣,這個remote容器的檔案remoteEntry.js也會打包出來,是專門用來給别人使用的。
new ModuleFederationPlugin({ // 使用子產品聯邦插件并配置
filename: "remoteEntry.js", // 建構輸出的檔案名,也就是打包出來的檔案名
name: "remote", // name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為${name}/${expose}
exposes: { // 被遠端引用時可暴露的資源路徑及其别名
"./NewsList": "./src/NewsList", // 這裡的key雖然說是暴露的元件,但是key還是要寫成路徑的形式。這個路徑的意思是代表目前remote容器根路徑下的NewsList(相對于remote容器根路徑)
},
remotes: {
remoteX:'[email protected]://localhost:6886/remoteEntry.js'
},
shared: { // 當目前項目作為host的時候,引用遠端容器,遠端容器的react和react-dom會複用目前項目的react和react-dom
react: { singleton: true}, // 如果有一個容器已經引用了react了,那麼另外一個容器就會複用react
'react-dom': {singleton: true} // 如果有一個容器已經引用了react了,那麼另外一個容器就會複用react
}
})
]
}
/*
A項目引用B,那麼A是host,B是遠端容器
1. B用exposes暴露元件,A中remotes引用元件
2. 如果AB共享某個庫,那麼shared在A中配置,也就是說共享的内容是在host中配置的
*/
// App.js
import React from "react";
import Sliders from './Sliders';
// 引用遠端容器(元件),傳回的就是一個元件RemoteNewsList
const RemoteNewsList = React.lazy(() => import('remoteY/NewsList')); // 動态加載遠端元件,引用'remote/NewsList',這個`remote`就是webpack配置的remotes中的key,也就是别名,`/NewsList`就是遠端那個元件配置的webpack中exposes下的key
const App = () => (
<div>
<h2>本地元件Sliders</h2>
<Sliders />
<React.Suspense fallback={<div>加載中</div>}>
<RemoteNewsList/>
</React.Suspense>
</div>
);
export default App;
- B應用
// webpack.config.js
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); // 需要引入子產品聯邦的插件
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
// publicPath: "http://localhost:3000/",
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
devServer: {
port: 3000
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
},
},
exclude: /node_modules/,
},
]
},
plugins: [
new HtmlWebpackPlugin({
template:'./public/index.html'
}),
// 下面這個插件的作用就是,這個項目打包後會産出名字為${filename}的檔案,并且這個容器叫做${name},同時會向外提供${exposes}的key對應的那些元件,value是向外提供的元件在目前項目中是哪個元件
// 另外,這個remote打包的産物,并不會覆寫項目的output.filename。兩者是獨立的。output中配置的會打包出來成為項目的産出,跟我們普通的打包一樣,這個remote容器的檔案remoteEntry.js也會打包出來,是專門用來給别人使用的。
new ModuleFederationPlugin({ // 使用子產品聯邦插件并配置
filename: "remoteEntry.js", // 建構輸出的檔案名,也就是打包出來的檔案名
name: "remote", // name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為${name}/${expose}
exposes: { // 被遠端引用時可暴露的資源路徑及其别名
"./NewsList": "./src/NewsList", // 這裡的key雖然說是暴露的元件,但是key還是要寫成路徑的形式。這個路徑的意思是代表目前remote容器根路徑下的NewsList(相對于remote容器根路徑)
},
remotes: {
remoteX:'[email protected]://localhost:6886/remoteEntry.js'
},
shared: { // 當目前項目作為host的時候,引用遠端容器,遠端容器的react和react-dom會複用目前項目的react和react-dom
react: { singleton: true}, // 如果有一個容器已經引用了react了,那麼另外一個容器就會複用react
'react-dom': {singleton: true} // 如果有一個容器已經引用了react了,那麼另外一個容器就會複用react
}
})
]
}
/*
A項目引用B,那麼A是host,B是遠端容器
1. B用exposes暴露元件,A中remotes引用元件
2. 如果AB共享某個庫,那麼shared在A中配置,也就是說共享的内容是在host中配置的
*/
// App.js
import React from "react";
import NewsList from './NewsList';
const RemoteSliders = React.lazy(() => import("remoteX/Sliders"))
const App = () => (
<div>
<h2>本地元件NewsList</h2>
<NewsList />
<React.Suspense fallback={<div>加載中</div>}>
<RemoteSliders/>
</React.Suspense>
</div>
);
export default App;
子產品聯邦面試題可能會問到實作原理、資料傳輸等問題,這個我專門會寫一個微前端專題。
最後,分享webpack5變更連結:changelog-v5/README.md at master · webpack/changelog-v5 · GitHub