微前端介紹
一、為什麼要學習微前端
什麼是微前端
微前端就是將不同的功能按照不同的維度拆分成多個子應用。通過主應用來載入這些子應用。
微前端的核心在於拆, 拆完後在合!
為什麼去使用微前端
- 不同團隊間開發同一個應用技術棧不同怎麼破?
- 希望每個團隊都可以獨立開發,獨立部署怎麼破?
- 專案中還需要老的應用程式碼怎麼破?
我們是不是可以將一個應用劃分成若干個子應用,將子應用打包成一個個的lib。當路徑切換時載入同的子應用。這樣每個子應用都是獨立的,技術棧也不用做限制了!從而解決了前端協同開發問題
怎樣落地微前端
微前端的靈感來源於,計算機上的應用,每一次使用者開啟一個應用,就相當於打開了一個新的頁面
- 2018年 Single-SPA誕生了, single-spa 是一個用於前端微服務化的 JavaScript 前端解決方案 (本身沒有處理樣式隔離, js 執行隔離) 實現了路由劫持和應用載入
- 2019年 qiankun 基於Single-SPA, 提供了更加開箱即用的 API ( single-spa + sandbox + import-html-entry ) 做到了,技術棧無關、並且接入簡單(像iframe 一樣簡單)
總結:子應用可以獨立構建,執行時動態載入,主子應用完全解耦,技術棧無關,靠的是協議接入(子應用必須匯出 bootstrap、mount、unmount方法)
這裡先回答下大家的問題:
-
這不是iframe嗎?
如果使用 iframe , iframe 中的子應用切換路由時使用者重新整理頁面就尷尬了
更多參考Why Not Iframe
-
應用之間怎麼通訊
- 基於URL來進行資料傳遞,但是傳遞訊息能力弱
- 基於 CustomEvent 實現通訊
- 基於props主子應用間通訊
- 使用全域性變數、 Redux 進行通訊
-
公共依賴
- CDN - externals
- webpack 聯邦模組
微前端架構具備以下幾個核心價值:
-
技術棧無關
主框架不限制接入應用的技術棧,微應用具備完全自主權
-
獨立開發、獨立部署
微應用倉庫獨立,前後端可獨立開發,部署完成後主框架自動完成同步更新
-
增量升級
在面對各種複雜場景時,我們通常很難對一個已經存在的系統做全量的技術棧升級或重構,而微前端是一種非常好的實施漸進式重構的手段和策略
-
獨立執行時
每個微應用之間狀態隔離,執行時狀態不共享
二、SingleSpa 實戰
Single-Apa完整的專案請參考 gitHub
構建子應用
我們需要父應用載入子應用,需要暴露三個方法:
1. bootstrap
2. mount
3. unmount
1. 構建子應用
// 啟動專案安卓依賴
vue create single-child npm i --save single-spa-vue
// main.js中匯入依賴 import singleSpaVue from 'single-spa-vue' const appOptions = { el: '#vue', // 掛載到父應用中的 id 為 vue 的標籤中 router, render: h => h(App) } const vueLifeCycle = singleSpaVue({ // 返回single-spa 的生命週期也就是 bootstrap/mount/unmount Vue, appOptions }); // single規定的協議,父應用會呼叫這些方法 export const bootstrap = vueLifeCycle.bootstrap; export const mount = vueLifeCycle.mount; export const unmount = vueLifeCycle.unmount; // 這樣做還有一個嚴重的問題,子應用無法啟動了??
2. 配置子應用中的打包路徑
// 配置vue.config.js module.exports = { configureWebpack: { output: { library: 'singleVue', libraryTarg: 'umd' }, devServer: { port: 10000 } } };
3. 配置子應用的路由
const router = new VueRouter({ mode: 'history', base: '/vue', // 配置路由的基礎路徑 routes })
4. 父應用搭建
vue create single-parent npm i --save single-spa // 注意這裡是single-spa
5. 將子應用掛載到id="vue"
的容器中
<div id="app"> <!-- 當路由切換到 /vue 時載入子應用 --> <router-link to="/vue">載入vue引用</router-link> <router-view/> <!-- 子應用載入的位置 --> <div id="vue"></div> </div>
6. 配置父應用載入子應用
import { registerApplication, start } from 'single-spa' async function loadScript(url) { // 非同步載入子元件中的指令碼 return new Promise((resolve, reject) => { let script = document.createElement('script'); script.src = url; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } registerApplication( 'myVueApp', async () => { console.log('載入模組'); // 載入子應用中的指令碼 await loadScript(`http://localhost:10000/js/chunk-vendors.js`) await loadScript(`http://localhost:10000/js/app.js`) // 這裡需要要返回 bootstrap/mount/unmount return window.singleVue }, location => location.pathname.startsWith('/vue'), // 此路徑用來判斷當前路由切換到 /vue 的路徑下,需要載入我們定義的子應用 { a: 1 } // 選傳,傳給子應用 props 的引數,可以是物件或值 ); start(); // 啟動應用
7. 配置子應用的路徑
// 設定路徑 if (window.singleSpaNavigate) { // 如果是父應用去應用,那會自動掛載一個屬性為true __webpack_public_path__ = 'http://localhost:10000/' }
8. 希望子應用可以獨立執行,在子應用中新增一個配置
if(!window.singleSpaNavigate){ delete appOptions.el; // 子應用中沒有#vue,所以需要手動刪除,掛載到 #app 中 new Vue(appOptions).$mount('#app'); }
singleSpa 缺陷
- 不能動態載入JS檔案
- 樣式不隔離
- 全域性物件,沒有JS沙箱的機制
三、qiankun 實戰
乾坤完整的demo請參考:gitHub
特點
- 簡單:任意 js 框架均可使用。微應用接入像使用接入一個 iframe 系統一樣簡單,但實際不是 iframe。
- 完備:幾乎包含所有構建微前端系統時所需要的基本能力,如 樣式隔離、js 沙箱、預載入等。
- 生產可用:已在螞蟻內外經受過足夠大量的線上系統的考驗及打磨,健壯性值得信賴。
專案構建
1. 主應用搭建qiankun-base
// 構建專案,下載依賴,只需要在主專案中安裝 qiankun 即可
vue create qiankun-base npm i --save qiankun
// 配置主專案的載入 main.js 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 Vue.use(ElementUI); const apps = [ { name: 'vueApp', // 應用的名字 entry: 'http://localhost:10000/', // 預設載入這個html,解析裡面的js動態的執行(子應用必須支援跨域,內部使用的是 fetch) container: '#vue', // 要渲染到的容器名id activeRule: '/vue' // 通過哪一個路由來啟用 }, { name: 'reactApp', entry: 'http://localhost:20000/', container: '#react', activeRule: '/react' } ]; registerMicroApps(apps); // 註冊應用 start(); // 開啟應用 new Vue({ router, render: h => h(App) }).$mount('#app')
<!-- 設定容器 --> <template> <div> <el-menu :router="true" mode="horizontal"> <!-- 主應用中也可以放自己的路由 --> <el-menu-item index="/">首頁</el-menu-item> <!-- 引用其他的子應用 --> <el-menu-item index="/vue">vue應用</el-menu-item> <el-menu-item index="/react">react應用</el-menu-item> </el-menu> <router-view v-show="$route.name"></router-view> <div id="vue"></div> <div id="react"></div> </div> </template>
2. 搭建Vue子專案
vue create qiankun-vue // 子專案中不需要安裝任何依賴,父元件會給window設定一些環境變數 // mian.js import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false /* new Vue({ router, render: h => h(App) }).$mount('#app') */ let instance = null; function render(props) { // props 元件通訊 instance = new Vue({ router, render: h => h(App) }).$mount('#app') // 這裡是掛載到自己的HTML中,基座會拿到這個掛載後的HTML,將其插入進去 } if (!window.__POWERED_BY_QIANKUN__) { // 如果是獨立執行,則手動呼叫渲染 render(); } if(window.__POWERED_BY_QIANKUN__){ // 如果是qiankun使用到了,則會動態注入路徑 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } // 根據 qiankun 的協議需要匯出 bootstrap/mount/unmount export async function bootstrap(props) { }; export async function mount(props) { render(props); }; export async function unmount(props) { instance.$destroy(); };
// 設定router路徑 const router = new VueRouter({ mode: 'history', base: '/vue', routes })
// 配置打包 vue.config.js module.exports = { devServer: { port: 10000, headers:{ 'Access-Control-Allow-Origin': '*' // 允許跨域 } }, configureWebpack: { output: { library: 'vueApp', libraryTarget: 'umd' } } };
3. 搭建React專案
npx create-react-app qiankun-react npm i --save-dev react-app-rewired // 入口配置 /src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; function render(){ ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); } if(!window.__POWERED_BY_QIANKUN__){ render(); } export async function bootstrap(){ } export async function mount() { render() } export async function unmount(){ ReactDOM.unmountComponentAtNode( document.getElementById('root')); }
// 配置啟動 config-overrides.js module.exports = { webpack:(config)=>{ config.output.library = 'reactApp'; config.output.libraryTarget = 'umd'; config.output.publicPath = 'http://localhost:20000/'; return config; }, devServer:(configFunction)=>{ return function (proxy,allowedHost){ const config = configFunction(proxy,allowedHost); config.headers = { "Access-Control-Allow-Origin":'*' } return config } } }
新增react環境變數 .env
PORT=20000
WDS_SOCKET_PORT=20000
// 配置react路由 import { BrowserRouter, Route, Link } from "react-router-dom" const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : ""; function App() { return ( <BrowserRouter basename={BASE_NAME}> <Link to="/">首頁</Link> <Link to="/about">關於</Link> <Route path="/" exact render={() => <h1>hello home</h1>}></Route> <Route path="/about" render={() => <h1>hello about</h1>}></Route> </BrowserRouter> ); }
完整的專案請參考 gitHub