微前端起源
微前端的概念最早由 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,可以引用remote中expose的模塊。
項目中如何使用
/**
* 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庫。
- remotes的代碼自己不打包,類似external,例如app2/button就是加載app2打包的代碼
- 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 的時候,會依賴 app2 的 app2.js,如果直接在 index.js 執(zhí)行,app2 的 app2.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。