一、什么是微前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends
微前端是一種多個團隊通過獨立發(fā)布功能的方式來共同構(gòu)建現(xiàn)代化 web 應(yīng)用的技術(shù)手段及方法策略省咨。
微前端的概念出現(xiàn)于2016年末绣夺,其將微服務(wù)的概念引入前端世界逸爵。用以解決在需求执俩、人員蚂维、技術(shù)棧等因素不斷更迭下前端工程演變成巨石應(yīng)用(Frontend Monolith)**而不可維護的問題背捌。這類問題尤其常見于企業(yè)級Web項目中毙籽。
微前端架構(gòu)具備以下幾個核心價值:
技術(shù)棧無關(guān)
主框架不限制接入應(yīng)用的技術(shù)棧,微應(yīng)用具備完全自主權(quán)獨立開發(fā)毡庆、獨立部署
微應(yīng)用倉庫獨立坑赡,前后端可獨立開發(fā)烙如,部署完成后主框架自動完成同步更新-
增量升級
在面對各種復(fù)雜場景時,我們通常很難對一個已經(jīng)存在的系統(tǒng)做全量的技術(shù)棧升級或重構(gòu)毅否,而微前端是一種非常好的實施漸進式重構(gòu)的手段和策略
獨立運行時
每個微應(yīng)用之間狀態(tài)隔離亚铁,運行時狀態(tài)不共享
微前端架構(gòu)旨在解決單體應(yīng)用在一個相對長的時間跨度下,由于參與的人員螟加、團隊的增多徘溢、變遷,從一個普通應(yīng)用演變成一個巨石應(yīng)用(Frontend Monolith)后捆探,隨之而來的應(yīng)用不可維護的問題然爆。這類問題在企業(yè)級 Web 應(yīng)用中尤其常見。
更多關(guān)于微前端的相關(guān)介紹黍图,推薦大家可以去看這幾篇文章:
二曾雕、 qiankun
qiankun 是螞蟻金服開源的一套完整的微前端解決方案。具體描述可查看 文檔 和 Github助被。
鏈接: https://qiankun.umijs.org/zh/guide 是qiankun的說明以及API教程 剖张。
2.1 qiankun 的核心設(shè)計理念
?? 簡單
由于主應(yīng)用微應(yīng)用都能做到技術(shù)棧無關(guān),qiankun 對于用戶而言只是一個類似 jQuery 的庫揩环,你需要調(diào)用幾個 qiankun 的 API 即可完成應(yīng)用的微前端改造搔弄。同時由于 qiankun 的 HTML entry 及沙箱的設(shè)計,使得微應(yīng)用的接入像使用 iframe 一樣簡單丰滑。
?? 解耦/技術(shù)棧無關(guān)
微前端的核心目標(biāo)是將巨石應(yīng)用拆解成若干可以自治的松耦合微應(yīng)用肯污,而 qiankun 的諸多設(shè)計均是秉持這一原則,如 HTML entry吨枉、沙箱蹦渣、應(yīng)用間通信等。這樣才能確保微應(yīng)用真正具備 獨立開發(fā)貌亭、獨立運行 的能力柬唯。
2.2 Why Not Iframe
為什么不用 iframe,這幾乎是所有微前端方案第一個會被 challenge 的問題圃庭。但是大部分微前端方案又不約而同放棄了 iframe 方案锄奢,自然是有原因的,并不是為了 "炫技" 或者刻意追求 "特立獨行"剧腻。
如果不考慮體驗問題拘央,iframe 幾乎是最完美的微前端解決方案了。
iframe 最大的特性就是提供了瀏覽器原生的硬隔離方案书在,不論是樣式隔離灰伟、js 隔離這類問題統(tǒng)統(tǒng)都能被完美解決。但他的最大問題也在于他的隔離性無法被突破儒旬,導(dǎo)致應(yīng)用間上下文無法被共享栏账,隨之帶來的開發(fā)體驗帖族、產(chǎn)品體驗的問題。
其實這個問題之前這篇也提到過挡爵,這里再單獨拿出來回顧一下好了竖般。
- url 不同步。瀏覽器刷新 iframe url 狀態(tài)丟失茶鹃、后退前進按鈕無法使用涣雕。
- UI 不同步,DOM 結(jié)構(gòu)不共享闭翩。想象一下屏幕右下角 1/4 的 iframe 里來一個帶遮罩層的彈框胞谭,同時我們要求這個彈框要瀏覽器居中顯示,還要瀏覽器 resize 時自動居中..
- 全局上下文完全隔離男杈,內(nèi)存變量不共享丈屹。iframe 內(nèi)外系統(tǒng)的通信、數(shù)據(jù)同步等需求伶棒,主應(yīng)用的 cookie 要透傳到根域名都不同的子應(yīng)用中實現(xiàn)免登效果旺垒。
- 慢。每次子應(yīng)用進入都是一次瀏覽器上下文重建肤无、資源重新加載的過程先蒋。
其中有的問題比較好解決(問題1),有的問題我們可以睜一只眼閉一只眼(問題4)宛渐,但有的問題我們則很難解決(問題3)甚至無法解決(問題2)竞漾,而這些無法解決的問題恰恰又會給產(chǎn)品帶來非常嚴(yán)重的體驗問題, 最終導(dǎo)致我們舍棄了 iframe 方案窥翩。
2.3 特性
- ?? 基于 single-spa 封裝业岁,提供了更加開箱即用的 API。
- ?? 技術(shù)棧無關(guān)寇蚊,任意技術(shù)棧的應(yīng)用均可 使用/接入笔时,不論是 React/Vue/Angular/JQuery 還是其他等框架。
- ?? HTML Entry 接入方式仗岸,讓你接入微應(yīng)用像使用 iframe 一樣簡單允耿。
- ?? 樣式隔離,確保微應(yīng)用之間樣式互相不干擾扒怖。
- ?? JS 沙箱较锡,確保微應(yīng)用之間 全局變量/事件 不沖突。
- ?? 資源預(yù)加載盗痒,在瀏覽器空閑時間預(yù)加載未打開的微應(yīng)用資源蚂蕴,加速微應(yīng)用打開速度。
- ?? umi 插件,提供了 @umijs/plugin-qiankun 供 umi 應(yīng)用一鍵切換成微前端架構(gòu)系統(tǒng)掂墓。
2.4 qiankun快速上手
2.4.1 主應(yīng)用
2.4.1.1 安裝 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
2.4.1.2 在主應(yīng)用中注冊微應(yīng)用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
當(dāng)微應(yīng)用信息注冊完之后,一旦瀏覽器的 url 發(fā)生變化看成,便會自動觸發(fā) qiankun 的匹配邏輯君编,所有 activeRule 規(guī)則匹配上的微應(yīng)用就會被插入到指定的 container 中,同時依次調(diào)用微應(yīng)用暴露出的生命周期鉤子川慌。
如果微應(yīng)用不是直接跟路由關(guān)聯(lián)的時候吃嘿,你也可以選擇手動加載微應(yīng)用的方式:
import { loadMicroApp } from 'qiankun';
loadMicroApp(
{
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
}
);
2.4.2 微應(yīng)用
微應(yīng)用不需要額外安裝任何其他依賴即可接入 qiankun 主應(yīng)用。
2.4.2.1 導(dǎo)出相應(yīng)的生命周期鉤子
微應(yīng)用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 導(dǎo)出 bootstrap
梦重、mount
兑燥、unmount
三個生命周期鉤子,以供主應(yīng)用在適當(dāng)?shù)臅r機調(diào)用琴拧。
/**
* bootstrap 只會在微應(yīng)用初始化的時候調(diào)用一次降瞳,下次微應(yīng)用重新進入時會直接調(diào)用 mount 鉤子,不會再重復(fù)觸發(fā) bootstrap蚓胸。
* 通常我們可以在這里做一些全局變量的初始化挣饥,比如不會在 unmount 階段被銷毀的應(yīng)用級別的緩存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 應(yīng)用每次進入都會調(diào)用 mount 方法沛膳,通常我們在這里觸發(fā)應(yīng)用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 應(yīng)用每次 切出/卸載 會調(diào)用的方法扔枫,通常在這里我們會卸載微應(yīng)用的應(yīng)用實例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 可選生命周期鉤子,僅使用 loadMicroApp 方式加載微應(yīng)用時生效
*/
export async function update(props) {
console.log('update props', props);
}
qiankun 基于 single-spa锹安,所以你可以在這里找到更多關(guān)于微應(yīng)用生命周期相關(guān)的文檔說明短荐。
無 webpack 等構(gòu)建工具的應(yīng)用接入方式請見這里。
2.4.2.2 配置微應(yīng)用的打包工具
除了代碼中暴露出相應(yīng)的生命周期鉤子之外叹哭,為了讓主應(yīng)用能正確識別微應(yīng)用暴露出來的一些信息忍宋,微應(yīng)用的打包工具需要增加如下配置:
webpack:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
2.4.3 官方項目實踐
參考鏈接:https://qiankun.umijs.org/zh/guide/tutorial。
三风罩、Vue+qiankun實現(xiàn)案例
首先分別創(chuàng)建我們的 項目基座 和 子項目 在這里我分別創(chuàng)建了 qiankun-base qiankun-vue qiankun-react 三個基礎(chǔ)的前端項目讶踪,直接用vue 和react的官方腳手架創(chuàng)建即可。
vue create qiankun-base
然后 install
或者add qiankun
vue create qiankun-vue
npx create-reacte-app qiankun-react
為了美觀 創(chuàng)建結(jié)束后 在基座應(yīng)用中 引用一下 element-ui 泊交,基座的app.vue這里就是簡單的配置了一個項目路由的顯示乳讥。
<template>
<div>
<el-menu :router="true"
mode="horizontal">
<!-- 基座內(nèi)不可以放自己的路由 -->
<el-menu-item index="/">Home</el-menu-item>
<!-- 引用vue子路由 -->
<el-menu-item index="/vue">vue應(yīng)用</el-menu-item>
<!-- 引用react子路由 -->
<el-menu-item index="/react">react應(yīng)用</el-menu-item>
</el-menu>
<router-view></router-view>
<div id="vue"></div>
<div id="react"></div>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
然后再main.js中正式注冊子項目 詳細(xì)代碼意義 的內(nèi)容代碼里做了注釋。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Elementui from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import { registerMicroApps, start } from 'qiankun'
// Vue.config.productionTip = false
const apps = [
{
name: 'vueApp', // 應(yīng)用名
entry: 'http://localhost:8081/', // 默認(rèn)會加載這個html 解析里面的js 動態(tài)的執(zhí)行 (子應(yīng)用必須支持跨域)
// fetch
container: '#vue', // 容器
activeRule: '/vue' // 激活路由
},
{
name: 'reactApp',
entry: 'http://localhost:8082/', // 默認(rèn)會加載這個html 解析里面的js 動態(tài)的執(zhí)行 (子應(yīng)用必須支持跨域)
// fetch
container: '#react',
activeRule: '/react'
}
]
registerMicroApps(apps, {
// beforeMount()
// beforeUnmount()
}) //注冊app +生命周期
start({
prefetch: false // 取消預(yù)加載
}) // 啟動
Vue.use(Elementui)
new Vue({
router,
render: h => h(App)
}).$mount('#app')
這里的registerMicroApps 和start 都是qiankun內(nèi)部的注冊方法 我就沒有過多的注釋詳細(xì)的內(nèi)容講解可以 查看官方的API文檔,其中也可以加一些app生命周期的操作,到這里其實基座的準(zhǔn)備已經(jīng)結(jié)束了 其實基座的作用就是承接一個子應(yīng)用的一個掛載 至于內(nèi)部的樣式隔離 js 隔離qiankun在掛載的時候已經(jīng)做了處理 在這里就不需要我們另外處理了廓俭。
接下來我們修改我們的子應(yīng)用云石。
main.js。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// Vue.config.productionTip = false
let instance = null
function render (props) {
instance = new Vue({
router,
//store:[],
render: h => h(App)
// props:{}
}).$mount('#app') // 掛在到自己的HTML中 基座中會拿到這個掛載好的最終html 將其插入
}
if (window.__POWERED_BY_QIANKUN__) { // 判斷是否為 qiankun掛載 不是的話自行啟動掛載
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
} else {
render();
}
// export const mount = async () => render();
// 子組件的渲染
export async function bootstrap (props) { };
export async function mount (props) {
render(props)
};
export async function unmount (props) {
instance.$destroy()
};
這里面有幾個變量是qiankun的內(nèi)置API變量和幾個掛載方法研乒,這里的render的判斷條件 是為了區(qū)分獨立運行和注入兩種狀態(tài)汹忠。
然后配置我們的vue.config.js。
module.exports = {
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: 'vueApp',
libraryTarget: 'umd'
}
}
}
設(shè)置允許所有人訪問 和導(dǎo)出模式,這樣我們的vue子應(yīng)用就配置完成了宽菜。
react 其實也是大同小異 但是為了改變react 默認(rèn)的配置 我們需要安裝一個插件 react-app-rewired
谣膳。
然后改變package.json配置。
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
創(chuàng)建 config-overrides.js铅乡。
module.exports = {
webpack: (config) => {
config.output.library = 'reactApp'
config.output.libraryTarget = 'umd'
config.output.publicPath = '//localhost:8082'
return config
},
devServer: (configFunction) => {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost)
config.headers = {
"Access-Control-Allow-Origin": '*'
}
return config
}
}
}
內(nèi)容跟vue 基本一致继谚。
修改index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// import * as serviceWorker from './serviceWorker';
function render () {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
if (!window.__POWERED_BY_QIANKUN__) { // 判斷是否為 qiankun掛載 不是的話自行啟動掛載
render();
}
// export const mount = async () => render();
// 子組件的渲染
export async function bootstrap (props) { };
export async function mount (props) {
render(props)
};
export async function unmount (props) {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
};
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
// serviceWorker.unregister();
新建一個.env文件配置一下自己的啟動端口。
PORT=8082
WDS_SOCKET_PORT=8082
最后app.js:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { BrowserRouter, Route, Link } from 'react-router-dom'
function App () {
return (
<BrowserRouter basename="/react">
<Link to="/">首頁</Link>
<Link to="/about">關(guān)于</Link>
<Route path="/" exact render={() => (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
)}></Route>
<Route path="/about" exact render={() => (
<div>about頁面</div>
)}></Route>
</BrowserRouter>
);
}
export default App;
這樣我們的開發(fā)步驟基本就結(jié)束了阵幸。
接下來看成果: