微前端實踐一

微前端起源

微前端的概念最早由 thoughtworks 在 2016 年提出。其核心思路是借鑒后端微服務(wù)架構(gòu)理念,將一個單體的龐大的前端應(yīng)用拆分為多個簡單獨立的前端工程。每個前端工程可以獨立開發(fā)、測試、部署。最終再由一個容器應(yīng)用,將拆分后的微前端工程組合為一個整體,面向用戶提供服務(wù)

微前端的價值

  • 技術(shù)棧無關(guān)
    主框架不限制接入應(yīng)用的技術(shù)棧,子應(yīng)用具備完全自主權(quán)
  • 獨立開發(fā)、獨立部署
    子應(yīng)用倉庫獨立,前后端可獨立開發(fā),部署完成后主框架自動完成同步更新
  • 獨立運(yùn)行時
    每個子應(yīng)用之間狀態(tài)隔離,運(yùn)行時狀態(tài)不共享

微前端架構(gòu)旨在解決單體應(yīng)用在一個相對長的時間跨度下,由于參與的人員、團(tuán)隊的增多、變遷,從一個普通應(yīng)用演變成一個巨石應(yīng)用(Frontend Monolith)后,隨之而來的應(yīng)用不可維護(hù)的問題。這類問題在企業(yè)級 Web 應(yīng)用中尤其常見。

解決方案:

MPA: 多頁面應(yīng)用(Multi page web application)
SPA: 單頁面應(yīng)用(Single page web appliction)

MPA:

  • 優(yōu)點: 部署簡單、各應(yīng)用之間硬隔離,天生具備技術(shù)棧無關(guān)、獨立開發(fā)、獨立部署的特性。
  • 缺點則也很明顯,應(yīng)用之間切換會造成瀏覽器重刷,由于產(chǎn)品域名之間相互跳轉(zhuǎn),流程體驗上會存在斷點。

SPA

  • 優(yōu)點: 則天生具備體驗上的優(yōu)勢,應(yīng)用直接無刷新切換,能極大的保證多產(chǎn)品之間流程操作串聯(lián)時的流程性。
  • 缺點則在于各應(yīng)用技術(shù)棧之間是強(qiáng)耦合的。

常見的實現(xiàn)方式

  • 路由分發(fā)式。通過 HTTP 服務(wù)器的反向代理功能,來將請求路由到對應(yīng)的應(yīng)用上。
  • 前端微服務(wù)化。在不同的框架之上設(shè)計通訊、加載機(jī)制,以在一個頁面內(nèi)加載對應(yīng)的應(yīng)用。
  • 微應(yīng)用。通過軟件工程的方式,在部署構(gòu)建環(huán)境中,組合多個獨立應(yīng)用成一個單體應(yīng)用。
  • 微件化。開發(fā)一個新的構(gòu)建系統(tǒng),將部分業(yè)務(wù)功能構(gòu)建成一個獨立的 chunk 代碼,使用時只需要遠(yuǎn)程加載即可。
  • 前端容器化。通過將 iFrame 作為容器,來容納其它前端應(yīng)用。
  • 應(yīng)用組件化。借助于 Web Components 技術(shù),來構(gòu)建跨框架的前端應(yīng)用。

路由分發(fā)式

路由分發(fā)式微前端,即通過路由將不同的業(yè)務(wù)分發(fā)到不同的、獨立前端應(yīng)用上。其通??梢酝ㄟ^ HTTP 服務(wù)器的反向代理來實現(xiàn),又或者是應(yīng)用框架自帶的路由來解決。

前端微服務(wù)化

前端微服務(wù)化,是微服務(wù)架構(gòu)在前端的實施,每個前端應(yīng)用都是完全獨立(技術(shù)棧、開發(fā)、部署、構(gòu)建獨立)、自主運(yùn)行的,最后通過模塊化的方式組合出完整的前端應(yīng)用。其

組合式集成:微應(yīng)用化

微應(yīng)用化,即在開發(fā)時,應(yīng)用都是以單一、微小應(yīng)用的形式存在,而在運(yùn)行時,則通過構(gòu)建系統(tǒng)合并這些應(yīng)用,組合成一個新的應(yīng)用。

微件化

微件(widget),指的是一段可以直接嵌入在應(yīng)用上運(yùn)行的代碼,它由開發(fā)人員預(yù)先編譯好,在加載時不需要再做任何修改或者編譯。

前端容器化

前端容器 iframe 或 web components

Systemjs模塊化解決方案
https://github.com/systemjs/systemjs

systemjs 是一個最小系統(tǒng)加載工具,用來創(chuàng)建插件來處理可替代的場景加載過程,包括加載 CSS 場景和圖片,主要運(yùn)行在瀏覽器和 NodeJS 中。它是 ES6 瀏覽器加載程序的的擴(kuò)展,將應(yīng)用在本地瀏覽器中。通常創(chuàng)建的插件名稱是模塊本身,要是沒有特意指定用途,則默認(rèn)插件名是模塊的擴(kuò)展名稱。

通常它支持創(chuàng)建的插件種類有:

// CSS 
System.import('my/file.css!')

// Image 
System.import('some/image.png!image')

// JSON 
System.import('some/data.json!').then(function(json){})

// Markdown 
System.import('app/some/project/README.md!').then(function(html) {})

// Text 
System.import('some/text.txt!text').then(function(text) {})

// WebFont 
System.import('google Port Lligat Slab, Droid Sans !font')

System.register('name', [], function () { ... });

示例

<script src="system.js"></script>
<script type="systemjs-importmap">
{
  "imports": {
    "lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"
  }
}
</script>
<script type="systemjs-module" src="/js/main.js"></script>

webpack5 Module Federation

https://indepth.dev/posts/1173/webpack-5-module-federation-a-game-changer-in-javascript-architecture

1、模塊聯(lián)邦是什么
簡單來說就是允許運(yùn)行時動態(tài)決定代碼的引入和加載。

app1
---index.js 入口文件
---bootstrap.js 啟動文件 // 特殊處理
---App.js react組件

app2
---index.js 入口文件
---bootstrap.js 啟動文件 // 特殊處理
---App.js react組件
---User.js react組件
---News.js react組件

2、代碼結(jié)構(gòu)

/** app1 **/

/**
 * index.js
 **/
import React from 'react'
import ReactDom from 'react-dom'

import App from './App'

ReactDom.render(<App />, document.getElementById('root'))


/**
 * App.js
 **/

 import React from 'react'

const User = React.lazy(() => import("app2/User"))

let _onbind = () => {
  console.log('onBind')
}
const App = () => (
  <div>
    <h2>App1 Content</h2>
    <hr/>
    <React.Suspense fallback="Loading app2">
      <User name={'app1 named'} onbind={ _onbind}/>
    </React.Suspense>
  </div>
)

export default App

暫時不用關(guān)心app2的代碼,問題關(guān)鍵是: app1是如何引入app2的代碼的?

3、Module federation的配置

/**
 * app1/webpack.config.js
 **/
 {
  plugins:[
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'public/index.html')
    }),
    new Mfp({
      filename:'app1.js',// 對外提供打包后的文件名,導(dǎo)入時會使用
      name:'app1',// 微應(yīng)用的名字
      remotes: {  // 引用外部的組件
        app2: "app2@http://localhost:3001/app2.js",
      },
      // shared: ["react", "react-dom"],
      shared: {
        react: { singleton: true }, // singleton 只實例化一次
        "react-dom": { singleton: true }
      }
    })
  ]
 }
  • 配置:exposes/remotes
    app1項目引入 app2 的 News組件 User組件
/**
 * app2/webpack.config.js
 **/
new Mfp({
  filename:'app2.js',// 對外提供打包后的文件名,導(dǎo)入時會使用
  name:'app2',// 微應(yīng)用的名字
  exposes:{ // 暴露外部的組件
    './News':'./src/News.js', // 名字:具體那個一個組件
    './User':'./src/User.js',
  },
})

/**
 * app1/webpack.config.js
 **/
new Mfp({
  filename:'app1.js',// 對外提供打包后的文件名,導(dǎo)入時會使用
  name:'app1',// 微應(yīng)用的名字
  remotes: { // 引用外部的組件
    app2: "app2@http://localhost:3001/app2.js",
  },
})

我們重點關(guān)注 exposes/remotes

  • 提供了 exposes 選項的表示當(dāng)前應(yīng)用是一個 Remote,exposes 內(nèi)的模塊可以被其他的 Host 引用,引用方式為 import(${name}/${expose})。
  • 提供了 remotes 選項的表示當(dāng)前應(yīng)用是一個 Host,可以引用 remoteexpose 的模塊。

項目中如何使用

/**
 * app1/App.js中通過 React.lazy 引用
 * 使用 <React.Suspense></React.Suspense>包括
 **/
import React from 'react'
const User = React.lazy(() => import("app2/User"))

const App = () => (
  <div>
    <h2>App1 Content</h2>
    <hr/>
    
    <React.Suspense fallback="Loading app">
      <User/>
    </React.Suspense>
  </div>
)

export default App
  • 配置:shared
    除了前面提到的模塊引入和模塊暴露相關(guān)的配置外,還有個 shared 配置,主要是用來避免項目出現(xiàn)多個公共依賴。
    例如,我們當(dāng)前的項目 app1,已經(jīng)引入了一個 react/react-dom,而項目 app2 暴露的User組件也依賴了 react/react-dom。如果不解決這個問題,項目 app1 就會加載兩個 react 庫。
    1. remotes的代碼自己不打包,類似external,例如app2/button就是加載app2打包的代碼
    1. shared的代碼自己是有打包的
  • 問題及解決方案
    1、配置shared后報錯: Shared module is not available for eager consumption
    [圖片上傳失敗...(image-989a0e-1628564569501)]

解決方案:
增加bootstrap.js 通過 index.js 異步加載頁面

/**
 * webpack.config.js
 **/
const config = {
  module: {
    rules: [
      {
        test: /bootstrap\.js$/,
        loader: 'bundle-loader',
        options: {
          lazy: true,
        },
      },
    ]
  }
}

/**
 * index.js
 **/
import bootstrap from './bootstrap'
bootstrap()

/**
 * bootstrap.js
 **/
import React from 'react'
import ReactDom from 'react-dom'

import App from './App'
ReactDom.render(<App />, document.getElementById('root'))


主要原因是 remote 暴露的 js 文件需要優(yōu)先加載,如果 bootstrap.js 不是一個異步邏輯,在 import User 的時候,會依賴 app2app2.js,如果直接在 index.js 執(zhí)行,app2app2.js 根本沒有加載,所以會有問題。

  • 雙向共享
/**
 * app1/webpack.config.js
 **/
new Mfp({
  filename:'app1.js',
  name:'app1',
  exposes:{
    // 名字:具體那個一個組件
    './Button':'./src/Button.js',
  },
})

/**
 * app2/webpack.config.js
 **/
 new Mfp({
  filename:'app2.js',
  name:'app2',
  // 引用外部的組件
  remotes: {
    app1: "app1@http://localhost:3000/app1.js",
  },
})

/**
 * app2/News.js
 **/
 import React from 'react'

const Button = React.lazy(() => import("app1/Button"))
const News = () => (
  <div>
    App2 News組件
    <React.Suspense fallback="loading app1">
      <Button />
    </React.Suspense>
  </div>
)

export default News

這里有一個點需要特別注意,就是入口文件 index.js 本身沒有什么邏輯,反而將邏輯放在了 bootstrap.js 中,index.js 去動態(tài)加載 bootstrap.js。

參考文檔:
https://micro-frontends.org/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容