qiankun微前端项目实战

561 阅读6分钟

qiankun微前端架构

本教程是视频教程对应的笔记,配合视频教程更容易理解

架构概念

比后端的微服务架构晚一些。

以前开发项目后端都是一个项目。团队开发管理自己分支。合并在一起打包运行。

随着后端业务越来越复杂,一个项目业务容易出现相互影响。

一个大的项目拆分微很多单体项目(服务),在通过微服务器架构实现,每个应用之间的通信。

养车系统后端采用微服务架构。

image-20231109100455997

核心概念:

  1. 解决一个项目里面需要涵盖多种技术的问题。可以采用微前端的方式设计
  2. 项目业务模块非常多,后续可能拆分应用。或者模块单独的销售等等。

搭建流程

项目创建三个

  1. Vue3的技术栈 Vue+ts来作为主应用。
  2. Vue2技术栈+js来实现的子应用一
  3. React+js技术栈来实现的子应用二

会将Vue3作为主应用。

React和Vue2作为子应用。

主应用开发(Vue3)

主应用中你自己搭建好 路由环境、状态机环境、Elementplus的环境。

(1)依赖下载

在应用中下载qiankun插件

yarn add qiankun

(2)配置变量

在src/quankun/index.ts

/**
 * apps里面存放的就是子应用
 */
const apps: any = [

]

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

/**
 * 注册微应用
 */
registerMicroApps(apps, {
    // qiankun 生命周期钩子 - 微应用加载前
    beforeLoad: (app) => {
        console.log("before load", app.name);
        return Promise.resolve();
    },
    // qiankun 生命周期钩子 - 微应用挂载后
    afterMount: (app) => {
        console.log("after mount", app.name);
        return Promise.resolve();
    },
});

/**
 * 处理全局异常:子应用可能出现加载失败
 */
addGlobalUncaughtErrorHandler((event) => {
    console.error(event);
    const { message: msg } = event;
    // 加载失败时提示
    if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
        alert("子应用加载失败")
    }
});


export default start

(3)加载qiankun

src/main.ts代码中配置qiankun

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router"
import startQuankun from "./qiankun"

const app = createApp(App)
app.use(router)
app.mount('#app')

startQuankun()

startQuankun就是启动的函数。

(4)配置加载容器

<template>
  <div class="wrapper">
    <ul>
      <li>主应用</li>
      <li>React微应用</li>
      <li>Vue微应用</li>
    </ul>
    <div>
      <!-- router-view渲染当前项目路由 -->
      <router-view v-show="$route.name"></router-view>
      <!-- 加载子应用 -->
      <div v-show="!$route.name" id="container"></div>
    </div>
  </div>
</template>

<script lang='ts' setup>
import {useRouter,useRoute} from "vue-router"
import {onMounted} from "vue"

const $router = useRouter()
const $route = useRoute()



</script>

<style scoped>
.wrapper{
  width:100%;
  height:100vh;
  display:flex;
  
}
.wrapper ul{
    width:200px;
    height:100vh;
    background-color:pink;
  }
</style>

核心概念:主应用有自己的路由,子应用也需要渲染。在父应用中考虑到底渲染自己页面,还是加载子应用页面

React子应用

微前端项目中,每个应用是可以独立运行的。独立打包

也可以放在一起运行。配置了qiankun的信息,不影响我们项目自己启动运行访问

项目创建完毕后,自己先搭建好路由。或者状态机

(1)修改端口

有可能你们有多个React子应用。每个React项目启动端口默认都是3000

需要在React项目根目录下面创建.env

PORT=8000

(2)配置好React自己路由

在App.jsx这个文件中配置

import React from 'react'
import { Link, BrowserRouter, Routes, Route } from "react-router-dom"
import Shop from './pages/Shop'
import Category from './pages/Category'

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route index element={<Shop></Shop>}></Route>
        <Route path="/category" element={<Category></Category>}></Route>
      </Routes>
    </BrowserRouter>
  )
}

(3)注册微应用

你想要qiankun能加载我们当前这个项目,并作为子应用。

那就必须在主应用中,将这个项目的信息配置注册到主应用

Vue3项目/src/qiankun/index.ts

/**
 * apps里面存放的就是子应用
 * name:子应用名字。这个名字在子应用会订好,不是随便写的名字
 * entry:两个应用通信地址,这个地址可能远程服务器
 * container:子应用加载完毕后,在哪个容器中渲染。
 * activeRule:触发子应用加载的路由
 */
const apps: any = [
    {
        name:"ReactApp",
        entry:"//localhost:8000",
        container:"#container",
        activeRule:"/react"
    }
]

(4)配置React子应用

src/index.js文件

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

if (window.__POWERED_BY_QIANKUN__) {
  // 动态设置 webpack publicPath,防止资源加载出错
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(<App />);
}

// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * 生命周期函数:由qiankun产生的
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("ReactApp bootstraped");
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("ReactApp mount", props);
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("ReactApp unmount");
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}

(5)配置路由基础路径

import React from 'react'
import { Link, BrowserRouter, Routes, Route } from "react-router-dom"
import Shop from './pages/Shop'
import Category from './pages/Category'
const BASE_NAME = window.__POWERED_BY_QIANKUN__?"/react":"/"

export default function App() {
  return (
    <BrowserRouter basename={BASE_NAME}>
      <Routes>
        <Route index element={<Shop></Shop>}></Route>
        <Route path="/category" element={<Category></Category>}></Route>
      </Routes>
    </BrowserRouter>
  )
}

子应用独立访问的时候,默认/就是基础路径,如果qiankun来访问,默认带着/react来访问我们路由。

我们需要判断设置基础路径。

(6)webpack的配置

父应用要和子应用通信。

  1. 跨域问题。
  2. 子应用打包出来代码,是否能够被qiankun加载

需要在子应用配置webpack

react/webpack配置

npm install react-app-rewired -D

在React/package.json

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

在React项目中根目录下面创建一个config-overrides.js

module.exports = {
    webpack: (config) => {
        config.output.library = `ReactApp`;
        config.output.libraryTarget = 'umd';
        // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
        config.output.chunkLoadingGlobal = `webpackJsonp_ReactApp`;
        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;
    },
};

打包后项目名字ReactApp

打包后结果是umd格式

配置这台80000服务器,允许跨域访问。

(7)切换保存

错误信息如下

根据错误信息排查出来,问题出在ReactApp这个子应用中。

image-20231110093146269

排查问题:

ReactApp这个应用被卸载完成后ReactDOM已经被资源回收了。无法在执行下面代码

export async function unmount() {
  console.log("ReactApp unmount");
  ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}

解决方案:

let root = null
export async function unmount() {
  console.log("ReactApp unmount");
  // ReactDOM.unmountComponentAtNode(document.getElementById("root"));
  if (root) {
    //销毁资源
    root.unmount();
    root = null;
  }
}

(8)指定在某个路由中映射React应用

(1)在主应用中配置路由
const routes = [
    {
        path: "/login",
        name: "Login",
        component: LoginVue
    },
    {
        path: "/home",
        name: 'Home',
        component: HomeVue,
        children: [
            
            {
                path: "menu",
                name: "Menu",
                component: Menu
            },
            {
                path: 'react',
                redirect: "/home/react/"
            },
            {
                path: 'react/:pathMatch(.*)',
                name: 'React',
                component: () => import('../views/subs/React.vue'),
            },
        ]
    }
]

我想要React子应用在Home组件中进行渲染。路由就应该配置/home

并且当我们访问/home/react这个路由的时候。默认代表进入React.vue这个组件

(2)在React.vue组件中加载子应用
<template>
  <div id="container"></div>
</template>

<script lang='ts' setup>
</script>

<style lang='scss' scoped>
</style>
(3)配置子应用启动路径
const apps: any = [
    {
        name:"ReactApp",
        entry:"//localhost:8000",
        container:"#container",
        activeRule:"/home/react"
    }
]

activeRule这个属性要变成/home/react才能触发子应用加载

(4)在React子应用配置
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/home/react" : "/"

保持主应用访问的路由和子应用能够映射在一起。

子应用路由才能访问成功,否则无法匹配。