qiankun 微前端實踐及常見問題


關注公眾號: 微信搜索 前端工具人 ; 收貨更多的干貨

一、介紹:

qiankun 項目實際搭建, 及各種微應用流行框架技術 (vue2 、vue3、react 、 umi2 、umi3)的配置;

初衷:自己當時摸索qiankun構建項目時,問題百出, 特別是umi2 umi3,百度了幾天才把熱門框架都集合完畢;

目的:總結出的模板項目, 便於自己后期重構項目技術選型及項目快速搭建;也為其他有需要的朋友提供示例及參考;

項目源碼:已上傳到 github https://github.com/laijinxian/qiankun-template 如有對你有幫助,麻煩 star 下

末尾的常見問題多數為目前開發中遇到的疑難點, 特地整理出來;有其他問題歡迎留言交流

實際項目源碼就沒貼出來了,都是依據這個模板構建的;

后面看下好不好把實際項目源碼抽離出來,上傳到github; 目前子項目使用的是 vite2.0 + vue3 + ts 以及 react + Umi3 + dva + ts

二、什么是微前端 qiankun 篇

推薦閱讀 qiankun文檔

其實我個人更喜歡叫成 前端微服務架構, 感覺逼格更高點...

2.1 官方介紹:

  • 微前端是一種多個團隊通過獨立發布功能的方式來共同構建現代化 web 應用的技術手段及方法策略 (有點高深...)

2.2 我的觀點:

  • 與技術棧無關、獨立開發、獨立部署、增量升級、獨立運行;

  • 拆分、細化、解耦你的巨無霸項目; 提升開發及打包部署效率;

  • 不在局限於一個項目只能使用一種技術,一個項目可以使用 N 種技術,擴展自己技術知識面;

  • 對於比如 大型 erp、OA 之類的系統, 微前端可以讓你更加的得心應手的開發;

  • 對於想重構公司辣眼睛的項目尤為合適;這也算我入手微前端的主要原因之一,下面會講到;

  • 順應時代潮流, 作為主流技術現在非常多的公司招聘面試基本都會問微前端,細化程度不一樣;

三、 為什么用qiankun, 為什么選擇qiankun

3.1 為什么用qiakun

自打進入公司,看到了現有的項目,我總結了幾點

  • 項目全都使用 Vue, 一直開發下去你會發現 React 忘得快差不多了;技術的局限性;
  • 現有項目代碼又臭又長,毫無規范;eslint、css預編譯啥都沒有;
  • 2-3層 for 循環,var之類的,粘貼復制無用代碼不刪除到處可見;公共代碼提取、接口統一處理、工具類編寫不存在的;
  • 一個項目同時出現 vue、jQuery兩個大框架;運行項目、熱編譯、你可以先上趟廁所;
  • 每期功能迭代,先要花大半天時間去熟悉這個代碼、還真不敢亂改,有毒、誰改誰后悔的那種
  • 想重構,奈何剛接手的時候項目已經很大了,並且不怎么熟悉業務,且不斷的加功能; 一時重構基本不可能;千萬級別的用戶量出問題了這鍋背不動, 時間也不允許

后面需求排期不是很緊湊,正直qiankun微前端很火,就想着使用qiankun微前端方案重構;

思路如下:

  • 目標 把一個項目按照菜單划分,一個大菜單分為一個子服務(子項目)
  • 剛開始原有項目全部划分為一個子服務,新加功能菜單划分為另一個子服務;這樣既保證原有項目不變,新項目完全使用新的框架及開發風格規范;
  • 時間充裕下情況下,慢慢把其他功能按照菜單划分成子服務,慢慢的最小粒度去重構項目

3.2 目前微前端方案有:

  • iframe
  • single-spa
  • qiankun 基於 single-spa 方案實現, 更強大更易上手

推薦閱讀 掘金大佬文章, 文章有詳細介紹及常見問題

四、 構建步驟

項目結構:

├── main-service    // 主應用
└── sub-service     // 微應用
    └── sub-react   // react 子應用
    └── sub-umi2    // umi2 子應用
    └── sub-umi3    // umi3 子應用
    └── sub-vue2    // vue2 子應用
    └── sub-vue3    // vue3 子應用

推薦閱讀:

4.1 項目結構組成

主應用

vue2.x + vuec-li3 主要業務功能就是登錄注冊及菜單;官方推薦主應用盡可能的簡單,不要涉及其他的業務功能

微應用

  • vue2.x + vue-cli3
  • vue3.x + vue-cli4 + typescript
  • react16
  • react16 + umi2 + dva
  • react16 + umi3 + dva

4.2 主應用配置

qiankun 只需要在主應用中引入,微應用不需要

yarn add qiankun # 或者 npm i qiankun -S

4.3 主應用 src 下 注冊微應用

主應用 src 下新建 qiankun/index.js

import {
  registerMicroApps,
  runAfterFirstMounted,
  setDefaultMountApp,
  start
} from "qiankun";
import store from '../store/index'
import { instance } from "../main";
import 'nprogress/nprogress.css'

/**
 * Step1 初始化應用(可選)
 */

function loader(loading) {
  if (instance && instance.$children) {
    // instance.$children[0] 是App.vue,此時直接改動App.vue的isLoading
    instance.$children[0].isLoading = loading;
  }
}

/**
 * Step2 注冊子應用
 */

const microApps = [
  {
    name: 'sub-vue2',
    developer: 'vue2.x',
    entry: '//localhost:7788',
    activeRule: '/sub-vue2',
  },
  {
    name: 'sub-vue3',
    developer: 'vue3.x',
    entry: '//localhost:7799',
    activeRule: '/sub-vue3'
  },
  {
    name: 'sub-react',
    developer: 'react16',
    entry: '//localhost:7755',
    activeRule: '/sub-react'
  },
  {
    name: 'sub-umi2',
    developer: 'umi2.x',
    entry: '//localhost:7766',
    activeRule: '/sub-umi2'
  },
  {
    name: 'sub-umi3',
    developer: 'umi3.x',
    entry: '//localhost:7733',
    activeRule: '/sub-umi3'
  }
]

const apps = microApps.map(item => {
  return {
    ...item,
    loader, // 給子應用配置加上loader方法
    container: '#subapp-container', // 子應用掛載的div
    props: {
      developer: item.developer, // 下發基礎路由
      routerBase: item.activeRule, // 下發基礎路由
      getGlobalState: store.getGlobalState // 下發getGlobalState方法
    }
  }
})

registerMicroApps(apps, {
  beforeLoad: app => {
    console.log('before load app.name====>>>>>', app.name)
  },
  beforeMount: [
    app => {
      console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
    }
  ],
  afterMount: [
    app => {
      console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name)
    }
  ],
  afterUnmount: [
    app => {
      console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name)
    }
  ]
})

/**
 * Step3 設置默認進入的子應用
 */
setDefaultMountApp('/sub-vue2')

/**
 * Step4 啟動應用
 */
start();

runAfterFirstMounted(() => {
  console.log("[MainApp] first app mounted");
});

export default apps

4.4 微應用導出生命周期鈎子

各種框架配置推薦閱讀 官方文檔

下面以 vue3.xreact umi3 為例; 其他微服務配置請前往 [github](https://github.com/laijinxian/qiankun-template) 源碼查看

子應用的名稱最好與父應用在 qiankun/index.js 中配置的名稱一致(這樣可以直接使用package.json中的name作為output

vue3.x 微應用

首先 vue create sub-vue3 創建項目

修改 main.js 導出生命周期函數
// @ts-nocheck
import "./public-path";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import routes from "./router";
import store from "./store";

let router = null;
let instance = null;

function render(props = {}) {
  const { container } = props;
  router = createRouter({
    history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? "/sub-vue3" : "/"),
    routes
  });

  instance = createApp(App);
  instance.use(router);
  instance.use(store);
  instance.mount(container ? container.querySelector("#app") : "#app");
}

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

export async function bootstrap() {
  console.log("%c ", "color: green;", "vue3.0 app bootstraped");
}

function storeTest(props) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value, prev) =>
        console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
      true
    );
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name
      }
    });
}

export async function mount(props) {
  storeTest(props);
  render(props);
  instance.config.globalProperties.$onGlobalStateChange =
    props.onGlobalStateChange;
  instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}

export async function unmount() {
  instance.unmount();
  instance._container.innerHTML = "";
  instance = null;
  router = null;
}

新建 vue.config.js
const path = require('path');
const { name } = require('./package');

function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  outputDir: 'dist',
  assetsDir: 'static',
  filenameHashing: true,
  devServer: {
    hot: true,
    disableHostCheck: true,
    port: '7799',
    overlay: {
      warnings: false,
      errors: true,
    },
    clientLogLevel: "warning",
    disableHostCheck: true,
    compress: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    historyApiFallback: true,
    overlay: { warnings: false, errors: true }
  },
  // 自定義webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src'),
      },
    },
    output: {
      // 把子應用打包成 umd 庫格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

src 新建 public-path.js 並引入
/* eslint-disable @typescript-eslint/camelcase */
if ((window as any).__POWERED_BY_QIANKUN__) {
  /* eslint-disable @typescript-eslint/camelcase */
  __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

react umi3 微應用

創建項目

推薦閱讀: umi 官方文檔

$ mkdir myapp && cd myapp
$ yarn create umi
安裝
$ npm install --save-dev @umijs/plugin-qiankun
$ yarn add @umijs/plugin-qiankun
修改 src/app.js 導出生命周期函數
import './public-path'

export const dva = {
  config: {
    onError(err) {
      err.preventDefault();
      console.error(err.message);
    },
  },
};

export const qiankun = {
  // 應用加載之前
  async bootstrap(props) {
    console.log('app1 bootstrap', props);
  },
  // 應用 render 之前觸發
  async mount(props) {
    console.log('app1 mount', props);
    storeTest(props);
  },
  // 應用卸載之后觸發
  async unmount(props) {
    console.log('app1 unmount', props);
  },
};

function storeTest(props) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
      true,
    );
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name,
      },
    });
}
修改 .umirc.js 文件 引入 @umijs/plugin-qiankun 插件
// ref: https://umijs.org/config/
export default {
  mountElementId: 'sub-umi3',
  base: `sub-umi3`, // 子應用的 base,默認為 package.json 中的 name 字段
  treeShaking: true,
  routes: [
    { exact: false, path: '/', component: '../layouts/index',
      routes: [
        { exact: false, path: '/', component: '../pages/index' },
        { component: './404.js' }
      ],
    }
  ],
  plugins: [
    ['@umijs/plugin-qiankun', {
      keepOriginalRoutes: true
    }],
    // ref: https://umijs.org/plugin/umi-plugin-react.html
    ['umi-plugin-react', {
      antd: true,
      dva: true,
      dynamicImport: { webpackChunkName: true },
      title: 'react',
      dll: false,
      
      routes: {
        exclude: [
          /models\//,
          /services\//,
          /model\.(t|j)sx?$/,
          /service\.(t|j)sx?$/,
          /components\//,
        ],
      },
    }],
  ],
}

src 新建 public-path.js 並引入
/* eslint-disable @typescript-eslint/camelcase */
if ((window as any).__POWERED_BY_QIANKUN__) {
  /* eslint-disable @typescript-eslint/camelcase */
  __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

以上就是微前端的基本配置 demo, 源碼 [github](https://github.com/laijinxian/qiankun-template) 查看

接下來真正的項目重構實操及進階

五、 項目重構實踐、進階中常見問題

5.1 qiankun 常見報錯

推薦閱讀: 官方文檔總結 https://qiankun.umijs.org/zh/faq

5.2 狀態管理, 主應用和微應用之間的通信

qiankun 通過 initGlobalState: 定義全局狀態,並返回通信方法,建議在主應用使用,微應用通過 props 獲取通信方法;

onGlobalStateChange: 在當前應用監聽全局狀態,有變更觸發 callback;

setGlobalState: 按一級屬性設置全局狀態,微應用中只能修改已存在的一級屬性; 換句話說只能修改主用於預先定義的屬性,后面添加的屬性無效

官方列子 發布-訂閱的設計模式
主應用

import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 變更后的狀態; prev 變更前的狀態
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

微應用

// 從生命周期 mount 中獲取通信方法,使用方式和 master 一致
export function mount(props) {

  props.onGlobalStateChange((state, prev) => {
    // state: 變更后的狀態; prev 變更前的狀態
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

5.3 各應用之間的獨立倉庫以及聚合管理

實際開發中項目存儲在公司倉庫中,以 gitLab 為例, 當子應用一多,全部放在一個倉庫下面, 這時候就顯得很臃腫了,也很龐大,大大的增加了維護成本,和開發效率;

我們可以通過 sh 腳本, 初始只需要克隆主倉庫代碼, 然后通過 sh 腳本去一鍵拉取所有子應用;

主倉庫新建 script/clone-all.sh 文件 內容如下

# 子服務 gitLab 地址
SUB_SERVICE_GIT=('http://gitlab.qinlinkeji.com/xxxxxx/qiankun-sub-service-vue.git' 'http://gitlab.qinlinkeji.com/xxxxxx/qiankun-sub-service-react.git')
SUB_SERVICE_NAME=('qiankun-sub-service-vue' 'qiankun-sub-service-react')

# 子服務
if [ ! -d "sub-service" ]; then
  echo '創建sub-service目錄...'
  mkdir sub-service
fi
echo '進入sub-service目錄...'
cd sub-service


# 遍歷克隆微服務
for i in ${!SUB_SERVICE_NAME[@]}
do
  if [ ! -d ${SUB_SERVICE_NAME[$i]} ]; then
    echo '克隆微服務項目'${SUB_SERVICE_NAME[$i]}
    git clone ${SUB_SERVICE_GIT[$i]}
  fi
done

echo '腳本結束...'
# 克隆完成

代碼拉取完成后, 緊接着就是下載各個項目的依賴及運行

應用根目錄安裝 npm i npm-run-all -D
package.json 文件 scripts 命令如下

"scripts": {
    "clone:all": "bash ./scripts/clone-all.sh",
    "install": "npm-run-all --serial install:*",
    "install:main": "cd main-service && cnpm i",
    "install:sub-vue2": "cd  sub-service/sub-vue2 && yarn install",
    "install:sub-vue3": "cd  sub-service/sub-vue3 && yarn install",
    "install:sub-react": "cd sub-service/sub-react && cnpm i",
    "install:sub-umi2": "cd sub-service/sub-umi2 && yarn install",
    "install:sub-umi3": "cd sub-service/sub-umi3 && yarn install",
    "start": "npm-run-all --parallel start:*",
    "start:sub-react": "cd sub-service/sub-react && npm start",
    "start:sub-vue2": "cd sub-service/sub-vue2 && npm start",
    "start:sub-vue3": "cd sub-service/sub-vue3 && yarn start",
    "start:sub-umi2": "cd sub-service/sub-umi2 && yarn start",
    "start:sub-umi3": "cd sub-service/sub-umi3 && yarn start",
    "start:main": "cd main-service && yarn start",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

步驟: 第一步 clone 主應用, 然后依次執行 yarn clone:all --> yarn install --> yarn start 即可運行整個項目

5.4 子應用之間的獨立開發

需求: 每次項目的迭代,有可能只涉及其中某個應用功能,

期望: 只需要單獨打開這個子應用修改即可;並不是整個龐大項目一起啟用

目標: 應用解耦的同時也能高效擼代碼

問題: 整個項目中都需要一個登錄態(登錄憑證 token), 上面說到登錄token是在主應用中維護的, 不啟動主應用,子應用怎么拿到登錄態token呢;

解析: 其實登錄的主要作用都是獲取到用戶信息及 token 后, 保存在瀏覽器緩存中,比如 localStorage、sessionStorage、cookie、IndexedDB , 需要的地方獲取即可;

方法: 子應用中通過 qiankun 提供的 window.__POWERED_BY_QIANKUN__ 屬性, 很直接的知道目前是否運行在 qiankun 的主應用的上下文中;全局維護一個變量,控制是否展示 iframe的登錄頁

if (!window.__POWERED_BY_QIANKUN__) {
  // 不在主應用的上下文中
}

當不在qiankun主應用上下文環境中時, 通過 iframe 形式, 直接引入登錄頁面, 完成登錄把用戶數據及token存入緩存中即可;

要注意的是:

  • 瀏覽器默認不支持iframe文件的 script 腳本執行; 需要設置 sandbox="allow-scripts allow-same-origin" 兩個屬性即可
  • 下面代碼是通過本地的 html 文件 (登錄頁);在vue-cli3中我們需要吧 html 靜態、文件放在 public/static下面
  • 當然當你項目發布到服務器之后, 把上面步驟刪了,直接在iframe里引用登錄頁面即可; iframeurl 指向你的線上登錄頁; 這樣下來子應用只需要加個 iframe 一行代碼,即可完成子應用的登錄態獲取

實例: vue 子應用 某頁面

<template>
  <!-- <iframe src="https://juejin.cn/" width="400" height="300" sandbox="allow-scripts allow-same-origin"></iframe> -->
  <iframe ref="iframe" name="iframe" width="400" height="300" sandbox="allow-scripts allow-same-origin"></iframe>
</template>
<script>
export default {
  data () {
    return {
      html: require("static/login.html")
    }
  },
  mounted() {
    this.$refs.iframe.srcdoc = this.html
    console.log(localStorage.getItem('userInfo'))
  }
}
</script>

5.5 如何提取出公共的依賴庫

官方說法: 並不推薦這種做法, 因為微服務主要目標是解耦大型應用, 並且當你升級某個項目的公共依賴之后,意味着其他子應用也升級了, 很難保證不出問題;但你確實想那么做,那么也有方法:

方法1: 官方推薦你可以在微應用中將公共依賴配置成 external,然后在主應用中導入這些公共依賴;
推薦閱讀: 掘金文章

方法2: 我的做法是 通過 webpackDllPlugin 動態鏈接庫, 生成靜態 json 在子應用中引
入; DllPluginwebpack內置的插件,不需要額外安裝; 這里就不貼代碼了, 代碼有點多, DllPlugin教程很多, 百度到處是, 跟着配置下webpack.dll.config.js就行;

DllPlugin 也是項目優化的一個手段, 自己配置一遍印象更深

5.6 如何提取出公共方法

在這我個人也不怎么推薦, 因為子應用是不同框架 vue\react\umi-react 並不能保證方法能同時作用於這幾個框架項目, 不能的話那何來公共方法一說;

當然你的子應用全是同一個框架那上面的話當我沒說。。。

有需求就有方法: 推薦參考 掘金文章 更詳細:

  • npm指向本地file地址:npm install file:../common。直接在根目錄新建一個common目錄,然后npm直接依賴文件路徑。
  • npm指向私有git倉庫: npm install git+ssh://xxx-common.git
  • 發布到npm私服

demo 中我用的是第一種方法,當然不嫌麻煩可以選用第三種發布到npm私服,嫌私服難搭,可以用后台的manven私服,把你的公共代碼給后台讓讓后台發布; manven 私服按我的理解后台標配;

第一種方法 指向本地file地址事例; vue 子應用 main.js 引入你的本地公共代碼並注冊

import globalRegister from '../../../main-service/src/store/global-register'
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  storeTest(props);
  render(props);
  globalRegister(store, props)
}

5.7 微應用之間如何跳轉

  • 主應用和微應用都是 hash 模式,主應用根據 hash 來判斷微應用,則不用考慮這個問題。
  • 主應用根據 path 來判斷微應用

history 模式的微應用之間的跳轉,或者微應用跳主應用頁面,直接使用微應用的路由實例是不行的,原因是微應用的路由實例跳轉都基於路由的 base。有兩種辦法可以跳轉:

  1. history.pushState()mdn用法介紹
  2. 將主應用的路由實例通過 props 傳給微應用,微應用這個路由實例跳轉。
// 用法 第二、第三參數分別是子應用名稱及激活路由
history.pushState(null, 'sub-react', '/sub-react');

5.8 微應用文件更新之后,訪問的還是舊版文

項目上線后由於是獨立倉庫獨立開發獨立部署, 微應用文件更新之后,訪問的還是舊版文;

服務器需要給微應用的 index.html 配置一個響應頭:Cache-Control no-cache,意思就是每次請求都檢查是否更新。

Nginx 為例:

location = /index.html {
  add_header Cache-Control no-cache;
}

5.9 應用加載的資源會 404

原因是 webpack 加載資源時未使用正確的 publicPath

可以通過以下兩個方式解決這個問題:

a. 使用 webpack 運行時 publicPath 配置
qiankun 將會在微應用 bootstrap 之前注入一個運行時的 publicPath 變量,你需要做的是在微應用的 entry js 的頂部添加如下代碼:

__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;

關於運行時 publicPath 的技術細節,可以參考 webpack 文檔。

runtime publicPath 主要解決的是微應用動態載入的 腳本、樣式、圖片 等地址不正確的問題。

b. 使用 webpack 靜態 publicPath 配置
你需要將你的 webpack publicPath 配置設置成一個絕對地址的 url,比如在開發環境可能是:

{
  output: {
    publicPath: `//localhost:${port}`,
  }
}

5.10 如何部署

推薦閱讀 官方文檔 更詳細

主應用和微應用都是獨立開發和部署,即它們都屬於不同的倉庫和服務

場景:主應用和微應用部署到同一個服務器(同一個IP和端口)
如果服務器數量有限,或不能跨域等原因需要把主應用和微應用部署到一起。

通常的做法是主應用部署在一級目錄,微應用部署在二/三級目錄。

微應用想部署在非根目錄,在微應用打包之前需要做兩件事:

  1. 必須配置 webpack 構建時的 publicPath 為目錄名稱,更多信息請看 webpack 官方說明 和 vue-cli3 的官方說明

  2. history 路由的微應用需要設置 base ,值為目錄名稱,用於獨立訪問時使用。

部署之后注意三點:

  1. activeRule 不能和微應用的真實訪問路徑一樣,否則在主應用頁面刷新會直接變成微應用頁面。
  2. 微應用的真實訪問路徑就是微應用的 entryentry 可以為相對路徑。
  3. 微應用的 entry 路徑最后面的 / 不可省略,否則 publicPath 會設置錯誤,例如子項的訪問路徑是 http://localhost:8080/app1,那么 entry 就是 http://localhost:8080/app1/

以上問題大多數可前端官方文檔常見問題查看, 只不過不是很詳細, 有的需要自己去百度完善

六、參考鏈接

掘金: https://juejin.cn/post/6875462470593904653


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM