qiankun微前端:从入门到实践

236 阅读6分钟

什么是 Qiankun?

简单来说,Qiankun 是一个基于 single-spa 的微前端实现库。它让多个前端应用在一个页面上无缝运行。想象一下,你有一个大盒子,里面可以放很多小盒子,每个小盒子都是一个独立的应用,但它们又能很好地协作,就像一个现代的“橡皮泥”框架。

Qiankun 的核心是提供了一种新的应用架构思路,方便我们更好地拆分前端项目,让每个子应用都能单独开发和部署。

Qiankun 能干啥?

随着业务量增长,单一的前端项目会变得难以维护:

  • 应用复杂:功能越来越多,迭代越来越慢。想象一下,你有一个已经维护很多的vue2老项目,虽然很少修改但是依然在使用中,项目页面十分庞大,如果要全部迁移到vue3,可想而知这是一件十分苦难的事情,而且对于业务团队来说价值并不高,投入与产出比严重不匹配。
  • 团队协作:多个团队共同开发一个大项目时,代码冲突频繁。
  • 技术升级困难:单一技术栈限制了团队的技术创新,大型的前端团队很可能会同时使用不同的技术框架来负责不同的业务系统,这些项目如何串联起来。

Qiankun 帮助我们:

  • 加快迭代:各个子应用独立可部署,更新更迅速。
  • 技术栈灵活:不用担心团队在用不同的前端框架。
  • 隔离与集成:各个应用功能和样式都可以独立,但又可以很好地兼容运行。

那为啥不用iframe

  • 隔离性:iframe 天然提供样式和 JS 隔离,但是带来了通信复杂、SEO 难、体验不好等问题。
  • 加载性能:iframe 加载缓慢,因为每个 iframe 都是一个独立的文档,而 Qiankun 通过 JavaScript 方式加载,更轻量级。
  • 开发体验:iframe 限制多,布局麻烦,得不到想要的用户体验效果。Qiankun 提供更现代化的微前端方案,使得开发更高效,调试更方便。
  • UI 不同步,DOM 结构不共享:想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  • url 不同步:浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。

来个实战

OK,现在我们来完成qiankun的示例项目,一起了解如何接入qiankun框架。这是我们所使用的项目框架:

  • 主应用:Vue3
  • 子应用1:Vue3
  • 子应用2:React18

主应用 by Vue3

  • 首先使用Vite创建一个vue3的项目,然后再此基础上进行改造
  • 在main.ts中注册子应用并启动:
// qiankun
import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from 'qiankun'
  
// 注册子应用
registerMicroApps([
    // VUE3子应用
    {
      name: 'subAppVue3',
      entry: 'http://localhost:3002',
      container: '#subapp-vue3',
      // 当路由中包含有subv3时,激活子应用
      activeRule: location => {
        return location.pathname.includes('/subv3')
      },
    },
    // react子应用
    {
      name: 'subAppReact',
      entry: 'http://localhost:3003',
      container: '#subapp-react',
      // 当路由中包含有subreact时,激活子应用
      activeRule: location => {
        return location.pathname.includes('/subreact')
      },
    },
  ])
  // 启动 qiankun
  start({
    prefetch: 'all',
  })

  // 添加全局异常捕获(非必须)
  addGlobalUncaughtErrorHandler(handler => {
    console.log('qiankun异常捕获', handler)
  })

  • 配置子项目访问路由
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/subv3',
      name: 'subv3',
      component: SubAppView
    },
    {
      path: '/subreact',
      name: 'subreact',
      component: SubAppView
    },
  ]
})
  • 实现SubAppView
<template>
  <!--子APP渲染容器-->
  <section class="sub-container">
    <div id="subapp-vue3"></div>
    <div id="subapp-react"></div>
  </section>
</template>

<style>
.sub-container {
  padding: 50px 20px;
  outline: 1px dashed dodgerblue;
}
</style>

要注意的是,这里配置的渲染子应用的容器ID,要跟main.ts配置的子应用容器保持一致。

  • 入口切换页面 来实现一个入口页面,方便我们切换不同的理由
<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <HelloWorld msg="Qiankun Demo" />

      <nav>
        <RouterLink to="/">Main</RouterLink>
        <RouterLink to="/subv3">SubApp-Vue3</RouterLink>
        <RouterLink to="/subreact">SubApp-React</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />

</template>

Vue3子应用

对于子应用来说,我们不仅要考虑子应用被嵌入口主应用运行,还需要考虑单独运行要如何处理。 这里同样为了方便,我们仍然先使用vite脚手架创建一个全新的项目,在此基础上进行修改。

  • 修改app渲染逻辑

一般情况下,vue的app在创建后都是直接挂载在#app DOM进行渲染,但是作为子应用,我们必须要把渲染逻辑交给qiankun框架来完成:

// main.ts
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

const initQianKun = () => { 
    renderWithQiankun({
        bootstrap() {console.log('bootstrap');},
        mount(_props) {
            console.log('mount', _props);
            render(_props.container)
        },
        unmount(_props) {
            console.log('unmount', _props);
        },
        update: function (props) {console.log('update');}
    })
}
const render = (container: HTMLElement | null | undefined) => {
    const app = createApp(App)
    const appDom = container ? container : "#sub-app-v3"
    app.mount(appDom)
}
// 判断是否为乾坤环境,否则会报错iqiankun]: Target container with #subAppContainerVue3 not existed while subAppVue3 mounting!
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render(null)

这里进行运行环境判断,如果是子应用环境,则调用qiankun来进行app的渲染,否则则直接渲染。

  • 修改vite.config配置
// qiankun插件
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  plugins: [
    vue(),
    qiankun('subAppVue3', { useDevMode: true })
  ],
  server: {
    port: 3002,
    host: true
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

这样,就完成了对vue3子应用的改造,如果现在直接启动子应用,则会成功看到页面效果:

image.png

react子应用

react的改造步骤稍微多一些

  • 在 src 目录新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
  • 在入口文件为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
import './public-path'

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

export function render(props) {
  const { container } = props;
  const conDom = container ? container.querySelector('#react-root') : document.getElementById('react-root');
  console.log("conDom", conDom)
  const root = ReactDOM.createRoot(conDom);
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap() {
  console.log('React app bootstrapped');
}

export async function mount(props) {
  console.log('Props from main framework', props);
  render(props);
}

export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#react-root') : document.getElementById('react-root'));
}

跟vue3子应用类似,我们也需要处理直接渲染react的逻辑,同时将qiankun的各生命周期函数暴露出来,供qiankun进行调用。

  • 修改 webpack 配置 安装@rescripts/cli,并在根目录增加.rescriptsrc.js配置:
pnpm i -D @rescripts/cli
const { name } = require('./package.json');

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.globalObject = 'window';

    return config;
  },

  devServer: (_) => {
    const config = _;

    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.liveReload = false;

    return config;
  },
};

同时把启动脚本修改为rescripts

{
    "start": "PORT=3003 rescripts start",
    "build": "rescripts build",
    "test": "rescripts test",
    "eject": "rescripts eject"
  }

直接启动react项目,一切正常的话可以看下运行效果

image.png

整体运行效果

同时启动三个项目,然后通过切换路由来确认子项目是否都能成功加载:

  • 渲染主项目页面

image.png

  • 渲染vue3子应用

image.png

  • 渲染react子应用

image.png

示例源码

gitee.com/open4jj/qia…

示例源码放在gitee上了,有需要的同学请自取,欢迎一键三连!