1. 程式人生 > 實用技巧 >微前端介紹

微前端介紹

一、為什麼要學習微前端

什麼是微前端

微前端就是將不同的功能按照不同的維度拆分成多個子應用。通過主應用來載入這些子應用。

微前端的核心在於拆, 拆完後在合!

為什麼去使用微前端

  • 不同團隊間開發同一個應用技術棧不同怎麼破?
  • 希望每個團隊都可以獨立開發,獨立部署怎麼破?
  • 專案中還需要老的應用程式碼怎麼破?

我們是不是可以將一個應用劃分成若干個子應用,將子應用打包成一個個的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-spa官網

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 缺陷

  1. 不能動態載入JS檔案
  2. 樣式不隔離
  3. 全域性物件,沒有JS沙箱的機制

三、qiankun 實戰

qiankun官網

乾坤完整的demo請參考:gitHub

特點

  1. 簡單:任意 js 框架均可使用。微應用接入像使用接入一個 iframe 系統一樣簡單,但實際不是 iframe。
  2. 完備:幾乎包含所有構建微前端系統時所需要的基本能力,如 樣式隔離、js 沙箱、預載入等。
  3. 生產可用:已在螞蟻內外經受過足夠大量的線上系統的考驗及打磨,健壯性值得信賴。

專案構建

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