基於Webpack5實現微前端架構

前言

最近這段時間微前端這個概念愈來愈被說起,它採用了微服務的相關理念,咱們能夠把一個應用拆分紅多個能夠互不依賴能夠獨立開發並單獨部署的模塊,而後在運行時把它們組合成一個完整的App。html

經過這樣的手段,咱們可使用不一樣的技術去開發應用的各個部分,好比這個模塊已經用React開發好了咱們能夠繼續用React,那個新模塊團隊更偏向於用Vue來實現咱們就能夠用Vue去實現。咱們能夠有專門的團隊去維護各個獨立的模塊,維護起來也會更加方便。這樣咱們團隊協做的方式也就跟着改變了。前端

Webpack5開始,已經內置了對微前端開發的支持,它們提供了一個新的功能叫Module Federation(我也不知道該怎麼翻譯這個術語會比較恰當),提供了足夠的能力來讓咱們實現微前端開發。react

話很少說,咱們仍是經過一個簡單的例子來感覺下總體的一個概念跟流程。咱們會實現一個簡單的App,而後把它經過webpack改形成微前端的形式。webpack

咱們開始吧!

此次全部配置都由咱們來手動完成。首先咱們新建一個空白目錄,而後在項目裏面執行:git

npm init -y
複製代碼

而後爲了使用webpack,github

npm add webpack webpack-nano -D
複製代碼

接下來咱們就能夠經過在根目錄新建一個webpack.config.js文件來配置整個打包過程啦!web

咱們在開發時跟運行時配置是有差異的,通常你們可能會編寫webpack.production.jswebpack.development.js兩個文件,來配置不一樣的環境。但這樣可能會讓咱們的配置對象變得很大很臃腫不容易維護,咱們須要在一大堆配置中找到咱們想要的配置去修改,並且各個環境的配置也不是徹底不一樣,那咱們得封裝啊,咱們得抽象啊,咱們要想辦法複用啊!npm

那咱們該怎麼辦呢?

咱們能不能把這個大的配置對象拆解成一個個具備特定功能的配置對象來單獨維護呢?json

好比咱們這個項目會經過mini-html-webpack-plugin來生成最終的index.html文件,那咱們就能夠寫一個單獨的函數來導出配置這個頁面的相關配置bootstrap

exports.page = ({title}) => ({
    plugins: [new MiniHtmlWebpackPlugin({
        context: {title}
    })]
})
複製代碼

這樣後續咱們要改變頁面相關的配置時就咱們就會知道來修改這個page函數,咱們甚至能夠替換成新的插件,而須要這個配置的地方只須要調用這個函數就能拿到配置,不須要關心細節,它們對咱們的變更是無感知的,天然也不會受到影響。咱們的配置也就能以函數的形式在各個環境中複用。

那麼問題來了,畢竟webpack最終仍是隻認它認識的那個配置形式,因此咱們還須要把這些函數返回的小配置對象合併成一個大的完整的配置對象。注意像Object.assign這種處理方式對數組不太友好,會丟失數據,你們能夠本身實現相關邏輯,或者使用webpack-merge這個包來處理。

爲了更好地管理webpack配置,不讓複雜的配置花了眼,咱們能夠再新建一個webpack.parts.js文件,在這裏定義一個個小函數來返回配置特定功能的配置對象。

而後在webpack.config.js裏面,咱們能夠導入這些函數,而且咱們能夠經過運行時傳過來的mode來判斷須要給什麼環境打包,動態生成最後的配置:

const {mode} = require('webpack-nano/argv')
const parts = require('./webpack.parts')
const {merge} = require('webpack-merge')

const commonConfig = merge([
    {mode},
    {entry: ["./App"]},
    parts.page({title: 'React Micro-Frontend'}),
    parts.loadJavaScript()
])

const productionConfig = merge([parts.eliminateUnusedCss()])

const developmentConfig = merge([{entry: ['webpack-plugin-serve/client']}, parts.devServer()])

const getConfig = (mode) => {
    process.env.NODE_ENV = mode
    switch (mode) {
        case 'production':
            return merge([commonConfig, productionConfig])
        case 'development':
            return merge([commonConfig, developmentConfig])
        default:
            throw new Error(`Trying to use an unknown mode, ${mode}`);
    }
}

module.exports = getConfig(mode)
複製代碼

這最大限度地避免了咱們配置文件的臃腫。

沖沖衝~

而後咱們還須要配置咱們的開發環境,咱們固然不想在開發時每次都手動去刷新頁面,這邊用到了一個插件webpack-plugin-serve來作實時更新:

exports.devServer = () => ({
    watch: true,
    plugins: [
        new WebpackPluginServe(
            {
                port: Process.env.PORT || 8000,
                host: '127.0.0.1',
                static: './dist',
                liveReload: true,
                waitForBuild: true
  })
    ]
})
複製代碼

而後咱們這邊使用了React做爲前端框架:

npm add react react-dom
複製代碼

爲了讓編譯器可以正確理解咱們的React組件,咱們要使用babel

npm add babel-loader @babel/core @babel/preset-env @babel/preset-react -D
複製代碼

配置一下babel-loader

exports.loadJavaScript = () => ({
    module: {
        rules: [
            { test: /\.js$/, include: APP_SOURCE, use: "babel-loader" },
        ],
    },
});
複製代碼

別忘了還要增長一個.babelrc文件

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
  }
    ],
    [
      "@babel/preset-react"
  ]
  ]
}
複製代碼

如今咱們的React組件能被正確處理了,咱們能夠開始寫咱們的組件了。

愉快的業務代碼環節~

首先是咱們的Header組件:

import React from "react";

const Header = () => {
    return <header>
        <h1>Micro-Frontend With React</h1>
    </header>
}

export default Header;
複製代碼

而後是咱們的Main組件:

import React from "react";
import Header from "./Header";

const Main = () => {
    return (
        <main>
            <Header/>
            <span>a Demo for Micro-Frontend using Webpack5</span>
        </main>
    );
}

export default Main
複製代碼

最後是入口文件:

import ReactDOM from "react-dom";
import React from "react";
import Main from "./Main";

const container = document.createElement("div");
document.body.appendChild(container);
ReactDOM.render(<Main/>, container);
複製代碼

打開package.json文件配置以下腳本:

"scripts": {
  "build": "wp --mode production",
  "start": "wp --mode development"
  }
複製代碼

如今咱們能夠經過在終端執行npm run start來預覽咱們的App了。

運行中的App

MF它來了!

接下來咱們來把它改形成微前端的形式,把Header作成單獨的模塊,而後其它的作成另一個模塊,這時候就要用到ModuleFederationPlugin了。

首先咱們要配置這個插件:

const {ModuleFederationPlugin} = require("webpack").container;

exports.federateModule = ({
                              name,
                              filename,
                              exposes,
                              remotes,
                              shared,
                          }) => ({
    plugins: [
        new ModuleFederationPlugin({
            name,
            filename,
            exposes,
            remotes,
            shared,
        }),
    ],
});
複製代碼

其中name是惟一ID,用於標記當前服務,filename是提供給其餘服務加載的文件,exposes則是須要暴露的模塊,remotes指定要使用的其它服務,shared則是配置公共模塊(好比lodash這種)

  • 提供了 exposes 選項的表示當前應用是一個 Remoteexposes 內的模塊能夠被其餘的 Host 引用,引用方式爲import(${name}/${expose})
  • 提供了 remotes 選項的表示當前應用是一個 Host,能夠引用 remoteexpose 的模塊。

咱們要在webpack.config.js裏面配置這兩個模塊:

const componentConfig = {
    App: merge(
        {
            entry: [path.join(__dirname, "src", "bootstrap.js")],
        },
        parts.page({title: 'React Micro-Frontend'}),
        parts.federateModule({
            name: "app",
            remotes: {mf: "mf@/mf.js"},
            shared: sharedDependencies,
        })
    ),
    Header: merge(
        {
            entry: [path.join(__dirname, "src", "Header.js")],
        },
        parts.federateModule({
            name: "mf",
            filename: "mf.js",
            exposes: {"./Header": "./src/Header"},
            shared: sharedDependencies,
        })
    ),
};
複製代碼

由於咱們爲了簡化代碼把全部代碼都寫在一個項目裏了,更常見的狀況是每一個模塊均可以有屬於本身的代碼倉庫,並且可使用不一樣的技術來實現。,這種狀況咱們處理的方式基本不變,引用遠程依賴時記得按照相似[name]@[protocol]://[domain]:[port][filename]的形式去指定remotes就好。

那爲了模擬多個項目獨立編譯,咱們也是用了組件名來設置不一樣的配置,這邊對於Header咱們並不想直接在瀏覽器中運行,而對於App咱們想要在瀏覽器中看到完整的頁面,因此咱們把對頁面相關的配置移到對App的配置中,這webpack.config.js在動態生成配置對象時也須要接受一個組件名做爲參數了。

const {mode, component} = require('webpack-nano/argv')
  ...
const getConfig = (mode, component) => {
    switch (mode) {
        case 'production':
            return merge([commonConfig, productionConfig, componentConfig[component]])
        case 'development':
            return merge([commonConfig, developmentConfig, componentConfig[component]])
        default:
            throw new Error(`Trying to use an unknown mode, ${mode}`);
    }
}
複製代碼

而後咱們要在Main裏修改引入Header的路徑

import Header from "mf/Header";
複製代碼

最後是要經過一個引導文件bootstrap.js來加載這一切

import("./App");
複製代碼

這是由於remote暴露的js文件須要優先加載,若是App.js不是異步的,在import Header的時候,會依賴mf.js,直接運行可能致使mf.js還沒有加載完畢,因此會有問題。

js加載順序

經過network面板也能夠看出,mf.js 是先於 App.js 加載的,因此咱們的 App.js 必須是個異步邏輯。

經過npm run build -- --component Header咱們先完成對Header的編譯,而後再經過npm run start -- --component App完成項目的運行,打開瀏覽器,應該能夠看到跟以前同樣的界面。

寫在最後

總的來講,這爲團隊協做代碼共享提供了新的方式,同時有一些侵入性,並且咱們的項目就得都依賴於webpack了。我我的以爲沒啥問題,畢竟如今大部分項目都會用到webpack,比較介意這一點的同窗能夠關注下vitevite利用瀏覽器原生的模塊化能力來提供代碼共享的解決方案。今天咱們僅僅用Module Federation實現了一個小demo,關於微前端webpack的管理都不是一篇文章就可以說得清楚的,還有不少事情能夠聊,我們後面再分別單獨展開講講,Happy coding~

完整Demo代碼

相關文章
相關標籤/搜索