Skip to content

qiankun微前端

qiankun 是一个基于 single-spa 的微前端实现库。

技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权

独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

独立运行时:每个微应用之间状态隔离,运行时状态不共享

官网:https://qiankun.umijs.org/zh/guide

主应用准备

主应用在 qiankun 架构中扮演着 "容器" 与 "协调者" 的角色,不限技术栈,负责整合、调度各个子应用资源,把控整体页面布局与路由逻辑。这里以 vite + vue3 为例。

1.初始化项目

bash
npm create vite@latest

2.安装路由

bash
npm i vue-router

3.新建 router/index.ts 文件

ts
const router = createRouter({
    // 在 qiankun 微前端框架中,主应用的路由模式选择会影响到子应用的加载和路由整合。虽然没有严格规定主应	   用不能使用 createHashHistory 模式,但在一些场景下, createWebHistory 模式更适合与 qiankun 		  配合使用。
    // 如果选用 hash 模式路径会变为:/#/main/#/yd-vue2,看着就挺奇怪
    // 尽量选用 history 模式
    history: createWebHistory()
})

4.安装 qiankun

bash
npm i qiankun

5.修改 main.ts

ts
// 引入qiankun
import { registerMicroApps, start } from 'qiankun'

// 进行子应用的注册
registerMicroApps([
  {
    name: 'dementia', // 子应用名称。取自子应用 package.json 文件中的 name 属性,必填
    entry: '//localhost:3003', // 子应用端口
    container: '#yd-container', // 子应用挂载的 dom
    activeRule: '/dementia', // 子应用的路由入口,URL包含此路径时加载该微应用
    props: {} // 传给子应用的数据
  }
])

start({
  // 是否开启沙箱,默认为 true
  sandbox: {
    // 样式隔离。为 true 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围
    /**
       假设应用名是 react16
       .app-main {
          font-size: 14px;
        }
          
       改写后的代码会表达类似为如下结构
        div[data-qiankun-react16] .app-main {
          font-size: 14px;
        }
     */
    experimentalStyleIsolation: true
  }
  // 是否开启单实例,单实例指的是同一时间只会渲染一个微应用。默认为 true
  // singular: true,
  // 是否开启预加载,默认为 true
  // prefetch:true
})

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

6.准备子应用展示容器

vue
<!-- 准备一个容器用于子应用展示,注意容器 id 必须与 main.ts 中 container 填写的一致-->
<div id="yd-container"></div>

子应用准备

1.初始化项目

bash
npm create vite@latest

2.安装 vite-plugin-qiankun

目前 qiankun 子应用并不支持 vite ,但可以借助此插件,让子应用也可以使用 vite。在此就以 vite + vue3 举例。

注意:vite-plugin-qiankun 需要 Node 20+

bash
npm install vite-plugin-qiankun -D

3.修改 vite.config.ts

ts
import qiankun from 'vite-plugin-qiankun'
import { name } from './package.json'

export default defineConfig({
    plugins: [
        vue(),
        // 接入qiankun环境
        qiankun(name, { useDevMode: true })
    ],
    server: {
        // 此处端口应与主应用中填写的地址保持一致
        port: 3003
    }
});

3.修改 main.ts

ts
import { createApp } from 'vue'
import App from './App.vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
import type { QiankunProps } from 'vite-plugin-qiankun/dist/helper'

let app: any = undefined

const render = ({ container }: QiankunProps = {}) => {
    app = createApp(App)
    // 避免 id 重复导致微应用挂载失败
    app.mount(container?.querySelector('#app') || '#app') 
}

// 非独立运行时,子应用需要提供三个生命周期钩子
const initQianKun = () => {
    renderWithQiankun({
        // 在子应用 首次加载或初始化时执行一次。当主应用通过 loadMicroApp 或路由匹配到子应用时,qiankun 会先调用 bootstrap 完成子应用的初始化。
        // 用于执行子应用的 全局初始化逻辑(如注册全局变量、初始化第三方库、配置全局状态管理等)。这些操作只需执行一次,后续子应用挂载(mount)时不会重复触发。
        bootstrap() {
            console.log('微应用:bootstrap')
        },
        // 在子应用 每次被激活(挂载)时执行
        // 用于 渲染子应用的 UI 界面 并绑定交互事件。子应用需要在此钩子中完成组件的挂载、数据绑定、事件监听等操作。
        mount(props) {
            // 获取主应用传入数据
            console.log('微应用:mount', props)
            render(props)
        },
        // 在子应用 被卸载或隐藏时执行
        // 用于清理子应用的资源,避免内存泄漏。子应用需要在此钩子中卸载组件、移除事件监听、清除定时器、重置全局状态等
        unmount(props) {
            console.log('微应用:unmount', props)
            app.unmount() //离开销毁
        },
        // 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
        update(props) {
            console.log('微应用:update', props)
        }
    })
}
// 判断当前是否是独立运行
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render({})

4.修改 router/index.ts

ts
const router = createRouter({
    // 需加上路由前缀,要与主应用中配置的 activeRule 保持一致
    history: createWebHistory('/dementia'),
    routes
})

实际案例

需求描述

主应用和子应用都是后台系统,页面结构一样,如下图所示

现在需要将子应用嵌入到主应用中,主应用只展示除内容部分的其他部分,子应用则只展示内容部分,切换页面则交给主应用侧边 menu 去做。如下图

实现注意点

1.主应用动态路由中添加兜底路由

ts
// 将兜底路由放置最后再添加。如果知道子应用路由也可添加对应路由映射到子应用的展示出口组件
Router.addRoute({
    // 匹配所有路由
    path: '/:pathMatch(.*)*',
    name: '407',
    // 遇到不认识的路由统一匹配到 layout 组件,也就是子应用的展示出口
    component: () => import('@/layout/index.vue')
})

2.直接将主应用二级路由出口包裹容器作为子应用容器(id="yd-container")

vue
<div>
    <!-- 面包屑组件 -->
    <Breadcrumb />
    <!-- 后台系统内容部分 -->
    <div id="yd-container">
        <router-view />
    </div>
</div>

应用通信

1.在主应用新建 qiankun.ts

ts
// 主要是通过发布订阅实现应用间通信
class Emitter {
    // 记录自定义事件
    private events: { [key: string]: Function[] } = {}
    // 添加自定义事件
    private add(eventName: string, callback: Function) {
        if (!eventName || typeof callback !== 'function') return
        if (!this.events[eventName]) {
            this.events[eventName] = []
        }
        this.events[eventName].push(callback)
    }
    // 发布自定义事件
    emit(eventName: string, ...args: any[]) {
        const callbacks = this.events[eventName]
        if (!callbacks) return
        callbacks.forEach(item => item.apply(this, args))
    }
    // 订阅自定义事件
    on(eventName: string, callback: Function) {
        this.add(eventName, callback)
    }
    // 解除订阅,不传递要解除订阅的函数则会清空该自定义事件下所有的订阅函数
    off(eventName: string, callback?: Function) {
        const callbacks = this.events[eventName] || []
        if (callbacks.length) {
            if (callback === undefined) return (this.events[eventName] = [])
            callbacks.find((item, index) => {
                if (item === callback) {
                    callbacks.splice(index, 1)
                    return true
                }
            })
        }
    }
    // 重置
    clear() {
        this.events = {}
    }
}
export default new Emitter()

2.在主应用 main.ts 中传递给子应用

ts
import Emitter from './utils/qiankun.ts'

registerMicroApps([
    {
        // 通过 props 传递给子应用使用
        props: { Emitter }
    }
])

3.在主应用发布或订阅事件

ts
// 发布 header 事件
function emit() {
  Emitter.emit('header', '主应用传递过来的属性')
}

// 订阅 loading 事件
Emitter.on('loading', (val: any) => {
  console.log('子应用事件', val)
})

4.在子应用中新建 qiankun.ts

ts
// 编写类型
type qiankunType = {
  emitter: {
    emit: (event: string, ...args: any[]) => void
    on: (event: string, fn: Function) => void
  }
}

// 用于保存主应用传递过来的实例
export let qiankun: qiankunType = {
  emitter: {
    emit: () => {},
    on: () => {}
  }
}

5.在子应用 main.ts 将主应用传递过来的实例保存起来

ts
const initQianKun = () => {
    renderWithQiankun({
        mount(props) {
            // 在 mount 生命周期函数中将主应用传递过来的实例保存起来
            qiankun.emitter = props.Emitter
            render(props)
        }
    })
}

6.在子应用发布或订阅事件

ts
// 发布 loading 事件
function emit() {
  qiankun.emitter.emit('loading', '子应用传递过来的属性')
}

// 订阅 header 事件
qiankun.emitter.on('header', (val: any) => {
  console.log('主应用事件', val)
})