qiankun微前端實踐

fozero發表於2023-09-20

為什麼要使用微前端

微前端架構具備以下幾個核心價值:

  • 技術棧無關
    主框架不限制接入應用的技術棧,微應用具備完全自主權
  • 獨立開發、獨立部署
    微應用倉庫獨立,前後端可獨立開發,部署完成後主框架自動完成同步更新
  • 增量升級在面對各種複雜場景時,我們通常很難對一個已經存在的系統做全量的技術棧升級或重構,而微前端是一種非常好的實施漸進式重構的手段和策略
  • 獨立執行時
    每個微應用之間狀態隔離,執行時狀態不共享

iframe存在問題

iframe最大的特性就是提供了測覺遊原生的使用離方案,不論是樣式隔離,is隔離這英問題統規定美解決。但他的最大問題也在於他的隔離性無法破突破,導致應用間上下文無法被共享,隨之帶來的開發體驗,產品體驗的問題

  • 瀏覽器的前進後退問題

iframe 頁面沒有自己的歷史記錄,共用父頁面的瀏覽歷史,當iframe 頁在內部進行跳轉時,瀏覽器位址列無變化,當瀏覽器重新整理時,無法停留在iframe內部跳轉後的頁面上,需要使用者重新走一遍操作

  • 元件複用問題,應用之間的元件無法複用--需要單獨提出來放到一個單獨的專案裡
  • iframe的彈窗及遮罩層問題

iframe 頁產生的彈窗,一般只能遮罩 iframe 區域。

  • 專案之間的通訊--只能使用postMessage
  • iframe裡的全屏問題--原生方法使用的是Element.requestFullscreen活使用vue-fullscreen外掛
  • 載入速度慢

每次子應用進入都是一次瀏覽器上下文重建,資源重新載入的過程。

qiankun介紹

官方文件 https://qiankun.umijs.org/zh/guide
qiankun 是一個基於 single-spa微前端實現庫,旨在幫助大家能更簡單、無痛的構建一個生產可用微前端架構系統。

qiankun 的核心設計理念

  • ? 簡單,由於主應用微應用都能做到技術棧無關,qiankun 對於使用者而言只是一個類似 jQuery 的庫,你需要呼叫幾個 qiankun 的 API 即可完成應用的微前端改造。同時由於 qiankun 的 HTML entry 及沙箱的設計,使得微應用的接入像使用 iframe 一樣簡單。
  • ? 解耦,技術棧無關微前端的核心目標是將巨石應用拆解成若干可以自治的松耦合微應用,而 qiankun 的諸多設計均是秉持這一原則,如 HTML entry、沙箱、應用間通訊等。這樣才能確保微應用真正具備 獨立開發、獨立執行 的能力。

特性

  • ? **基於 **single-spa 封裝,提供了更加開箱即用的 API。
  • ? 技術棧無關,任意技術棧的應用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架。
  • ? HTML Entry 接入方式,讓你接入微應用像使用 iframe 一樣簡單。
  • ? 樣式隔離,確保微應用之間樣式互相不干擾。
  • ? JS 沙箱,確保微應用之間 全域性變數/事件 不衝突。
  • ⚡️ 資源預載入,在瀏覽器空閒時間預載入未開啟的微應用資源,加速微應用開啟速度。
  • ? umi 外掛,提供了 @umijs/plugin-qiankun 供 umi 應用一鍵切換成微前端架構系統。

專案中接入qiankun

建立一個主應用和兩個子應用,這裡我使用的react18.2.0版本

主應用

  1. 建立主應用

npx create-react-app micro-main

  1. 執行並訪問 http://localhost:3000/

yarn start

  1. 安裝qiankun

yarn add qiankun

  1. 註冊子應用

在專案入口處頁面註冊子應用並啟動

import {
  registerMicroApps,
  start,
  addGlobalUncaughtErrorHandler,
  addErrorHandler,
} from "qiankun";

registerMicroApps([
  {
    name: "childOne",
    //預設會載入這個html,解析裡面的js,動態執行(子應用必須支援跨域)裡面,是用fetch去請求的資料
    entry: "//localhost:3001",
    //掛載到主應用的哪個元素下
    container: "#container",
    //當我劫持到路由地址為//child-one時,我就把http://localhost:3001這個應用掛載到#container的元素下
    activeRule: "/child-one",
  },
  {
    name: "childTwo",
    entry: "//localhost:3002",
    container: "#container",
    activeRule: "/child-two",
  },
]);
// 啟動 qiankun
start();
  1. 修改App.js元件新增導航連結,並新增#container元素,用於掛載子應用
import "./App.css";
import Home from "./pages/Home";

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect,
  Link,
} from "react-router-dom";

const Header = () => {
  return (
    <header>
      <div>微前端實踐-qiankun</div>
      <div>主應用</div>
      <nav>
        <Link to={"/"}>主應用首頁</Link>
        <Link to={"/child-one"}>訪問子應用1</Link>
        <Link to={"/child-two"}>訪問子應用2</Link>
      </nav>
    </header>
  );
};

function App() {
  return (
    <div className="App">
      <Router>
        <Header />
        <Switch>
          <Redirect from="/" to="/home" exact />
          <Route key="home" path="/home" exact={true} component={Home} />
        </Switch>
      </Router>
     {/* 載入子應用時,將其掛載到該#container元素下 */}
      <div id="container"></div>
    </div>
  );
}

export default App;

子應用

同樣,我們使用react建立兩個子應用
npx create-react-app pro-1
npx create-react-app pro-2

  1. 建立public-path.js檔案

src目錄下建立public-path.js檔案並在入口檔案引入,用於修改執行時的 publicPath
注意:執行時的 publicPath 和構建時的 publicPath 是不同的,兩者不能等價替代。

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 設定histroy模式路由base

建議使用 history 模式的路由,設定histroy模式路由base,值和它的 activeRule 是一樣的

<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/child-one' : '/'}>
  1. 修改入口檔案,並匯出幾個鉤子函式

分別對應bootstrap、mount、unmount、update幾個生命週期函式
為了避免根 id #root 與其他的 DOM 衝突,需要限制查詢範圍。

let root = null;

function render(props) {
  const { container } = props;
  // ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));

  root = ReactDOM.createRoot(
    container
      ? container.querySelector('#root')
      : document.getElementById("root")
  );
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}


/**
 * bootstrap 只會在微應用初始化的時候呼叫一次,下次微應用重新進入時會直接呼叫 mount 鉤子,不會再重複觸發 bootstrap。
 * 通常我們可以在這裡做一些全域性變數的初始化,比如不會在 unmount 階段被銷燬的應用級別的快取等。
 */
export async function bootstrap() {
  console.log("[react18] react app bootstraped");
}

/**
 * 應用每次進入都會呼叫 mount 方法,通常我們在這裡觸發應用的渲染方法
 */
export async function mount(props) {
  console.log("[react18] props from main framework", props);
  render(props);
}

/**
 * 應用每次 切出/解除安裝 會呼叫的方法,通常在這裡我們會解除安裝微應用的應用例項
 */
export async function unmount() {
  if (root) {
    root.unmount(); // 解除安裝元件
    root = null;
  }
}

/**
 * 可選生命週期鉤子,僅使用 loadMicroApp 方式載入微應用時生效
 */
export async function update(props) {
  console.log('update props', props);
}
  1. 修改 webpack 打包,允許開發環境跨域和 umd 打包。

這裡我們使用react-app-rewired來修改webpack配置,作用是可以在不eject的情況下自定義配置CRA腳手架建立的app
yarn add react-app-rewired --dev
在根目錄下建立config-overrides.js檔案

const { name } = require("./package");

module.exports = {
  webpack: function (config, env) {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = "umd";
    // webpack 5 需要把 jsonpFunction 替換成 chunkLoadingGlobal
    // config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
    config.output.globalObject = "window";
    return config;
  },

  devServer: (_) => {
    const config = _;
    config.headers = {
      "Access-Control-Allow-Origin": "*",
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

然後修改 package.json,scripts中將react-scripts替換成react-app-rewired即可
注意:eject不需要

"scripts": {
    "start": "PORT=3001 react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },

執行效果

問題總結

主應用訪問子應用,qiankun報錯情況

報錯1:Uncaught Error: application 'childOne' died in status LOADING_SOURCE_CODE: [qiankun]: Target container with #container not existed while childOne loading!
主應用container容器不存在導致無法掛載子應用
解決:不要將<div id="container"></div>放在Router標籤內,跟Router同級即可

報錯2:Uncaught TypeError: application 'childOne' died in status NOT_MOUNTED: container.getElementById is not a function
container.getElementById不是一個方法
解決:container情況下使用container.querySelector('#root')方式

function render(props) {
  const { container } = props;
  // ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));

  root = ReactDOM.createRoot(
    container
      ? container.querySelector('#root')
      : document.getElementById("root")
  );
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

報錯3:Uncaught (in promise) TypeError: Failed to fetch
子應用沒有執行的情況

當子應用服務關閉,主應用請求失敗時,如何配置跳轉404錯誤頁面?

qiankun的錯誤處理問題,當主應用請求不到子應用頁面時,如何設定跳轉到自定義的404頁面,而不是顯示錯誤
解決:
在app.ts中配置qiankun執行時配置,透過qiankun.addGlobalUncaughtErrorHandler()和qiankun.addErrorHandler()新增錯誤處理

import {
  addGlobalUncaughtErrorHandler,
  addErrorHandler,
} from "qiankun";

// 異常捕獲
addGlobalUncaughtErrorHandler((event) =>
  console.log("addGlobalUncaughtErrorHandler err", event)
);
addErrorHandler((err) => {
  console.log("addErrorHandler err", err);
});

檢視github程式碼

程式碼已經上傳到github,感興趣的歡迎clone & star
https://github.com/fozero/hello-qiankun

參考閱讀

相關文章