Skip to content

微前端qiankun 使用

多框架集成、子应用并行运行、跨应用通信

主应用注册微应用并启动

npm i qiankun -S
import { registerMicroApps, start } from 'qiankun'
import type { MicroAppStateActions } from 'qiankun'

interface SharedProps {
  token?: string;
  actions?: MicroAppStateActions;
  shareMainApp?: typeof sharedComponent;
}
const apps = [
  {
    name: 'vue3-project', // 子应用名称
    entry: 'http://localhost:8101/', // 子应用入口
    container: '#subAppContainer', // 放子应用的容器
    activeRule: '/vue3-project', // 激活子应用的路路由
    props: { // 传递给子应用的数据
      token: 'abcd23456',
      actions
    }
  },
  {
    name: 'react18-project',
    entry: 'http://localhost:8102/',
    container: '#subAppContainer',
    activeRule: '/react18-project',
    props: {
      shareMainApp: sharedComponent
    }
  },
  {
    name: 'umi-project',
    entry: 'http://localhost:8103/',
    container: '#subAppContainer',
    activeRule: '/umi-project'
  }
]
registerMicroApps<SharedProps>(apps, {
  beforeLoad: [async app => console.log('before load', app.name)],
  beforeMount: [async app => console.log('before mount', app.name)],
  afterMount: [async app => console.log('after mount', app.name)]
})

start({
  // prefetch: true, // 开启预加载,在浏览器空闲时,预先加载其他子应用的资源
  // prefetch: 'all', // 预加载所有子应用
  // prefetch: ['vue3-project', 'umi-project'], // 只预加载指定的应用
  // prefetch: (apps) => apps.filter(app => app.name !== 'test-app'),
  singular: false, // 多实例子应用共存
  sandbox: {
    // strictStyleIsolation: false,  // CSS 严格隔离,每个子应用被包裹在 Shadow DOM 中,样式完全隔离
    experimentalStyleIsolation: true // CSS 实验性隔离,Scoped CSS,qiankun 会给子应用的所有样式添加特殊前缀
  }
})

主应用设置跳转子应用的入口

const routes = createBrowserRouter([
  {
    path: '/',
    element: <LayoutPage />,
    children: [
      {
        index: true, // 默认打开
        element: <Home />
      },
      {
        path: 'vue3-project/?/*', // 通配符匹配 /vue3-project、 /vue3-project/goodsDetail
        element: <EmptyPage />
      },
      {
        path: 'react18-project/?/*',
        element: <EmptyPage />
      },
      {
        path: 'umi-project/?/*',
        element: <EmptyPage />
      }
    ]
  }
])

主应用设置子应用挂载的容器

const EmptyPage = () => {
  return <div id="subAppContainer"></div>
}
export default EmptyPage

vite 构建的子应用(vue3、 react18)

qiankun 不支持 vite,需要使用插件 vite-plugin-qiankun

npm i vite-plugin-qiankun -S
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  plugins: [
    vue(),
    qiankun('vue3-project', { // 与主应用注册的name保持一致
      useDevMode: true
    })
  ],
  server: { // 本地启动
    port: 8101,
    cors: true, // 允许跨域,主应用才能访问
    origin: "//localhost:8101", // 不加这个静态资源404
    headers: { 
      // 允许主应用访问子应用的资源(CORS 配置)
      'Access-Control-Allow-Origin': '*' 
    }
  },
  ...
})
export default defineConfig({
  plugins: [
    // react(),
    qiankun('react18-project', {
      useDevMode: true
    })
  ],
  server: {
    port: 8102,
    headers: { 'Access-Control-Allow-Origin': '*' }
  },
  ...
})

vue3+vite 子应用路由

import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

const router = createRouter({
  // 路由前缀
  history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/vue3-project/' : '/'),
  routes: [
    {
      path: '/',
      name: 'home',
      meta: {
        title: '首页'
      },
      component: HomeView
    }
    ...
})

react18+vite 子应用路由

import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

const routes = createBrowserRouter([
  {
    path: '/',
    element: <LayoutPage />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'article',
        element: <Article />,
        children: [
          {
            path: 'list',
            element: <ArticleList />
          },
          ...
        ]
      },
      ...
    ]
  },
  {
    path: '*',
    element: <NotFound />
  }
], { basename: qiankunWindow.__POWERED_BY_QIANKUN__ ? '/react18-project' : '/' }) // 路由前缀

vue3+vite 子应用 main.js 添加生命周期

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

let app = null

function render() {
  app = createApp(App)
  app.use(router)
  app.mount('#subApp')
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render()
}
renderWithQiankun({
  mount(props) {
    console.log('props', props)
    localStorage.setItem('xhhToken', props.token)
    render()
  },
  update() {},
  bootstrap() {},
  unmount() {
    app.unmount()
  }
})

react18+vite 子应用 main.tsx 添加生命周期

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

let root = null

function render() {
  root = createRoot(document.getElementById('root1')!)
  root.render(
    <ConfigProvider locale={zhCN} 
      theme={{
        token: {
          colorPrimary: '#990ff5'
        }
      }}>
      <RouterProvider router={router}></RouterProvider>
    </ConfigProvider>
  )
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render()
}
renderWithQiankun({
  mount(props) {
    render()
  },
  update() {},
  bootstrap() {},
  unmount() {
    root.unmount()
  }
})

umi 子应用配置

.env文件:

PORT=8103

config/config.ts:

import { defineConfig } from "umi"

// webpack相关的配置
export default defineConfig({
  base: '/umi-project',
  plugins: ['@umijs/plugins/dist/qiankun'],
  qiankun: {
    slave: {}
  },
  ...
});

src/app.ts:

暴漏qiankun生命周期

export const qiankun = {
  async bootstrap() {
    console.log('umi app ------ bootstrap')
  },
  async mount(props: any) {
    console.log('umi app ------ mount')
    console.log(props)
  },
  async unmount() {
    console.log('umi app ------ unmount')
  }
}

react19+webpack5 子应用配置

.env文件:

BROWSER=none
PORT=8104

或者

module.exports = {
  devServer: {
    port: 8104
  },
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    configure: (webpackConfig, { env, paths }) => {
      webpackConfig.output.library = "webpack-react";
      webpackConfig.output.libraryTarget = "umd";
      ...
      return webpackConfig;
    },
  }
}

路由前缀:

const router = createBrowserRouter([
  {
    path: '/',
    element: <Demo />
  },
  {
    path: '*',
    element: <div>404</div>
  }
], { basename: window.__POWERED_BY_QIANKUN__ ? '/create-react19-app' : '/' })

webpack配置:

webpackConfig.output.library = "webpack-react"
webpackConfig.output.libraryTarget = "umd"

public-path.js:

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

添加生命周期:

import './public-path.js'

let root = null
function render() {
  root = ReactDOM.createRoot(document.getElementById('root'))
  root.render(
    <Provider store={store}>
      <RouterProvider router={routerConfig} />
    </Provider>
  );
}

// 判断是否在qiankun环境下
if (!window.__POWERED_BY_QIANKUN__) {
  render({})
}

// 在微应用初始化的时候调用一次,下次重新进入微应用时会直接进入 mount
export async function bootstrap() {
  console.log("react19 app --- bootstrap")
}

export async function mount(props) {
  console.log("react19 app --- mount")
  render(props)
}

export async function unmount(props) {
  console.log("react19 app --- unmount")
  root?.unmount()
}

vue2+webpack3 子应用配置

设置端口号:

devServer: {
  port: 8105,
  headers: {
    'Access-Control-Allow-Origin': '*' // 子应用允许跨域
  }
}

路由前缀:

export default new Router({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue2-project' : '/',
  ...
  routes: [
    ...
  ]
})

webpack配置:

output: {
    library: 'vue2-project',
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_vue2`,
    publicPath: 'http://localhost:8105/' // 设置这个就不需要引入 public-path.js 了
    ...
  },  
}

public-path.js:

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line camelcase, no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
  // __webpack_public_path__ 值是:http://localhost:8105/
}

添加生命周期:

...
import './public-path.js'

let instance = null
function render () {
  instance = new Vue({
    el: '#app',
    router,
    store,
    components: { App },
    template: '<App/>'
  })
}

// 是否从qiankun主应用进入
if (!window.__POWERED_BY_QIANKUN__) {
  render({})
}

export async function bootstrap () {
  console.log('vue2 --- bootstrap')
}

export async function mount (props) {
  console.log('vue2 --- mount')
  render(props)
}

export async function unmount (props) {
  console.log('vue2 --- unmount')
  instance.$destroy()
  instance = null
}

dva 子应用配置

设置端口号:

scripts": {
  "dev": "cross-env BROWSER=none PORT=8107 SOCKET_SERVER=none roadhog server",
  ...
},

路由前缀:

import createHistory from 'history/createBrowserHistory'

function render() {
  app = dva({
    history: createHistory({ // history路由模式
      basename: window.__POWERED_BY_QIANKUN__ ? '/dva-project/' : '/'
    })
  })
  ...
}

新建 webpack.config.js:

module.exports = (webpackConfig, env) => {
  webpackConfig.output.library = "dva-project";
  webpackConfig.output.libraryTarget = "umd";
  webpackConfig.output.jsonpFunction = "webpackJsonp_dva";
  return webpackConfig
}

public-path.js:

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line camelcase, no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

添加生命周期:

import dva from 'dva';
import createHistory from 'history/createBrowserHistory'
...
import './public-path.js'

let app = null
function render() {
  app = dva({
    history: createHistory({
      basename: window.__POWERED_BY_QIANKUN__ ? '/dva-project/' : '/'
    })
  });
  window.dvaApp = app

  ...

  app.start('#rootdva')
}


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

export async function bootstrap() {
  console.log('dva--- bootstraped')
}

export async function mount(props) {
  console.log('dva--- mount')
  render(props)
}

export async function unmount() {
  console.log('dva--- unmount')
  app = null
  window.dvaApp = null
}

子应用跳到其他应用

// 跳到其他子应用
window.history.pushState({}, '', '/react18-project/article/list')
// 跳到主应用
window.history.pushState({}, '', '/');

启动应用

{
  "name": "xhh-qiankun-project",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "npm-run-all --parallel dev:*",
    "dev:main": "npm run dev --workspace main-app",
    "dev:vue": "npm run dev --workspace vue3-project",
    "dev:react": "npm run dev --workspace react18-project",
    "dev:umi": "npm run dev --workspace umi-project"
  },
  "workspaces": [
    "main-app",
    "vue3-project",
    "react18-project",
    "umi-project"
  ],
  "devDependencies": {
    "npm-run-all": "^4.1.5"
  },
  "dependencies": {
    "cheerio": "^1.0.0-rc.10",
    "vite-plugin-qiankun": "^1.0.15"
  }
}

共享组件

主应用组件

import CommonComp from '@/components/CommonComp'
const sharedComponent = {
  CommonComp
}

registerMicroApps([
  {
    name: 'react18-project',
    entry: 'http://localhost:8102/',
    container: '#subAppContainer',
    activeRule: '/react18-project',
    props: {
      shareMainApp: sharedComponent
    }
  }
  ...
])

子应用设置共享组件

utils/shareMainComp.ts:

let shareMainComponent = {}
interface ShareMainComponentResult {
  CommonComp: React.ComponentType;
}
export const getShareMainComponent = () => {
  return shareMainComponent as ShareMainComponentResult
}

export const setShareMainComponent = (currShareMainApp) => {
  for (const key in currShareMainApp) {
    if (Object.prototype.hasOwnProperty.call(currShareMainApp, key)) {
      shareMainComponent[key] = currShareMainApp[key]
    }
  }
}
import { setShareMainComponent } from '@/utils/shareMainComp'

renderWithQiankun({
  mount(props) {
    setShareMainComponent(props.shareMainApp)
    render()
  },
  ...
})

子应用使用组件:

import { getShareMainComponent } from '@/utils/shareMainComp'

function Home() {
  usePageTitle('首页')

  const { CommonComp } = getShareMainComponent()
  ...
}

<CommonComp />

应用通信


1. initGlobalState

主应用

initGlobalState设置全局状态:

import { initGlobalState, MicroAppStateActions } from 'qiankun'
   
const initialState = {}
const actions: MicroAppStateActions = initGlobalState(initialState)
   
export default actions

传递给子应用:

import actions from '@/utils/actions'

registerMicroApps([
  {
    name: 'vue3-project',
    entry: 'http://localhost:8101/',
    container: '#subAppContainer',
    activeRule: '/vue3-project',
    props: {
      actions
    }
  },
  ...
])

主应用修改全局状态:

import actions from '@/utils/actions'

function changeTheme(){
  if(theme === 'dark'){
    setTheme('light')
    actions.setGlobalState({ theme: 'light' })
  }else{
    setTheme('dark')
    actions.setGlobalState({ theme: 'dark' })
  }
}
子应用

先配置一个空的 actions 实例:

class Actions {
  actions = { onGlobalStateChange: () => {}, setGlobalState: () => {} }
  setActions(actions) { this.actions = actions }
  onGlobalStateChange(...args) { return this.actions.onGlobalStateChange(...args) }
  setGlobalState(...args) { return this.actions.setGlobalState(...args) }
}
let actions = new Actions()
export default actions

注入 actions:

renderWithQiankun({
  mount(props) {
    actions.setActions(props.actions)
    render()
  },
  unmount(props) {
    // 取消监听,避免内存泄露
    props.actions.offGlobalStateChange()
    app.unmount()
  }
  ...
})

监听全局状态:

actions.onGlobalStateChange((state, prev) => {
  console.log('全局状态改变:', state, prev)
}, true) // 第二个参数设为true, 初始化时立即触发一次

2. 自定义事件通信

// 应用1 发送事件
window.dispatchEvent(new CustomEvent('app-message', {
  detail: { msg: 'message from App A' }
}))

// 应用2 接收事件
window.addEventListener('app-message', (e) => {
  console.log(e.detail.msg)
})

3. props


4. storage

效果图

alt text


参考文档

https://qiankun.umijs.org/zh/api#initglobalstatestate