天天看點

30.webpack——webpack5新特性(啟動、持久化緩存、資源子產品、URIs、moduleIds和chunkIds、tree shaking、nodeJs的polyfill被移除、子產品聯邦)【重學webpack系列——webpack5.0】

【重學webpack系列——webpack5.0】

1-15節主要講webpack的使用,當然,建議結合《webpack學完這些就夠了》一起學習。

從16節開始,專攻webpack原理,隻有深入原理,才能學到webpack設計的精髓,進而将技術點運用到實際項目中。

可以點選上方專欄訂閱哦。

以下是本節正文:

1. webpack5有哪些新特性(

面試題

)

  1. 啟動指令
  2. 持久化緩存
  3. 資源子產品
  4. URIs
  5. moduleIds

    &

    chunkIds

    的優化
  6. 更智能的

    tree shaking

  7. nodeJs的

    polyfill

    腳本被移除
  8. 子產品聯邦

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

    =>

    asset/resource

    發送一個單獨的檔案并導出 URL
  • url-loader

    =>

    asset/inline

    導出一個資源的 data URI
  • asset 在導出一個 data URI 和發送一個單獨的檔案之間自動選擇。之前通過使用

    url-loader

    ,并且配置資源體積限制實作。當然asset也可以進行配置,根據配置來生成。
  • 資源子產品目前是實驗性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 都可以從緩存中去讀取,這樣就大大加快了打包速度。
30.webpack——webpack5新特性(啟動、持久化緩存、資源子產品、URIs、moduleIds和chunkIds、tree shaking、nodeJs的polyfill被移除、子產品聯邦)【重學webpack系列——webpack5.0】

7.新特性6——移除 Node.js 的 polyfill

  • webpack5 以前,webpack 會包含 nodejs 核心子產品的 polyfill,這樣的話,比如安裝了一個

    crypto

    子產品,那麼就可以直接使用,因為 node 的

    polyfill

    會自動啟動。
  • 現在,webpack5 移除了 nodejs 的 polyfill,無法再直接使用類似

    crypto

    的子產品了。如果你想要使用類似

    crypto

    的 nodejs 核心子產品,那麼可以在 webpack 配置檔案的

    resolve

    中配置

    fallback

    ,配置了就可以使用了。如果不需要引用,将其置為 false 就可以了
  • 開啟了 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 比較簡單,主要是找

    import

    進來的變量是否在這個子產品内出現過,出現過則不剔除,不出現過則剔除。并且用于 esModule 中
  • 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
    • 那麼這裡就有兩個概念:
      1. B是remote,需要暴露出去
      2. 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: "remote"

      name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為 n a m e / {name}/ name/{expose}
    • 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:
    • filename: "remoteEntry.js"

      建構輸出的檔案名,也就是打包出來的檔案名,這個名字個remote一樣沒事的,因為服務不一樣
    • name: "host"

      name是必須的配置,辨別輸出的子產品名,被遠端引用時路徑為 n a m e / {name}/ name/{expose}
    • 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

      ,這個

      remoteY

      就是webpack配置的remotes中的key,也就是别名,

      /NewsList

      就是遠端那個元件配置的webpack中exposes下的key(

      重點

      )
    • 傳回的就是一個元件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>
    );
               
  • 注意點

    • A項目引用B,那麼A是host,B是遠端容器
      • B用exposes暴露元件,A中remotes引用元件
      • 如果AB共享某個庫,那麼shared在A中配置,也就是說共享的内容是在host中配置的
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
            }
        })
    ]
               
  • 雙向依賴

    :
    • 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 }
                  }
            })
        ]
               
  • 多個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

繼續閱讀