Skip to content

qiankun ​

qiankun 是基于 single-spa 的微前端实现框架,开箱即用。

核心原理 ​

主应用(基座)
  │
  ├─ registerMicroApps([
  │     { name, entry, container, activeRule }
  │   ])
  │
  ├─ start({ sandbox, prefetch, ... })
  │
  └─ 子应用生命周期:bootstrap → mount → unmount
主应用(基座)
  │
  ├─ registerMicroApps([
  │     { name, entry, container, activeRule }
  │   ])
  │
  ├─ start({ sandbox, prefetch, ... })
  │
  └─ 子应用生命周期:bootstrap → mount → unmount

快速开始 ​

主应用 ​

js
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'vue-app',
    entry: '//localhost:7101',
    container: '#sub-app-container',
    activeRule: '/app/vue',
  },
  {
    name: 'react-app',
    entry: '//localhost:7102',
    container: '#sub-app-container',
    activeRule: '/app/react',
  },
])

start({
  sandbox: { experimentalStyleIsolation: true },
  prefetch: 'all',
})
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'vue-app',
    entry: '//localhost:7101',
    container: '#sub-app-container',
    activeRule: '/app/vue',
  },
  {
    name: 'react-app',
    entry: '//localhost:7102',
    container: '#sub-app-container',
    activeRule: '/app/react',
  },
])

start({
  sandbox: { experimentalStyleIsolation: true },
  prefetch: 'all',
})

子应用(以 Vue3 为例) ​

js
// main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'

let app = null
let router = null

export async function bootstrap() {
  console.log('子应用 bootstrap')
}

export async function mount(props) {
  const { container } = props
  router = createRouter({
    history: createWebHistory('/app/vue'),
    routes: [],
  })
  app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector('#app') : '#app')
}

export async function unmount() {
  app?.unmount()
  app = null
  router = null
}
// main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'

let app = null
let router = null

export async function bootstrap() {
  console.log('子应用 bootstrap')
}

export async function mount(props) {
  const { container } = props
  router = createRouter({
    history: createWebHistory('/app/vue'),
    routes: [],
  })
  app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector('#app') : '#app')
}

export async function unmount() {
  app?.unmount()
  app = null
  router = null
}

子应用 webpack 配置 ​

js
// vue.config.js
module.exports = {
  publicPath: '//localhost:7101',
  configureWebpack: {
    output: {
      library: 'vue-app',
      libraryTarget: 'umd',
      chunkLoadingGlobal: 'webpackJsonp_vue-app',
    },
  },
  devServer: {
    port: 7101,
    headers: { 'Access-Control-Allow-Origin': '*' },
  },
}
// vue.config.js
module.exports = {
  publicPath: '//localhost:7101',
  configureWebpack: {
    output: {
      library: 'vue-app',
      libraryTarget: 'umd',
      chunkLoadingGlobal: 'webpackJsonp_vue-app',
    },
  },
  devServer: {
    port: 7101,
    headers: { 'Access-Control-Allow-Origin': '*' },
  },
}

沙箱机制 ​

快照沙箱(LegacySandBox) ​

js
// 原理:在 mount 时快照 window,unmount 时还原
class SnapshotSandbox {
  constructor() {
    this.windowSnapshot = {}
    this.modifiedProps = {}
  }

  active() {
    this.windowSnapshot = {}
    for (const key in window) {
      this.windowSnapshot[key] = window[key]
    }
  }

  inactive() {
    for (const key in window) {
      if (window[key] !== this.windowSnapshot[key]) {
        this.modifiedProps[key] = window[key]
        window[key] = this.windowSnapshot[key]
      }
    }
  }
}
// 原理:在 mount 时快照 window,unmount 时还原
class SnapshotSandbox {
  constructor() {
    this.windowSnapshot = {}
    this.modifiedProps = {}
  }

  active() {
    this.windowSnapshot = {}
    for (const key in window) {
      this.windowSnapshot[key] = window[key]
    }
  }

  inactive() {
    for (const key in window) {
      if (window[key] !== this.windowSnapshot[key]) {
        this.modifiedProps[key] = window[key]
        window[key] = this.windowSnapshot[key]
      }
    }
  }
}

代理沙箱(ProxySandBox) ​

js
// 原理:通过 Proxy 拦截 window 操作,支持多实例
class ProxySandbox {
  constructor() {
    const fakeWindow = Object.create(null)
    const proxy = new Proxy(fakeWindow, {
      set(target, key, value) {
        target[key] = value
        return true
      },
      get(target, key) {
        return target[key] || window[key]
      },
    })
    this.proxy = proxy
  }
}
// 原理:通过 Proxy 拦截 window 操作,支持多实例
class ProxySandbox {
  constructor() {
    const fakeWindow = Object.create(null)
    const proxy = new Proxy(fakeWindow, {
      set(target, key, value) {
        target[key] = value
        return true
      },
      get(target, key) {
        return target[key] || window[key]
      },
    })
    this.proxy = proxy
  }
}

通信方案 ​

js
// 主应用
import { initGlobalState } from 'qiankun'
const actions = initGlobalState({ token: '' })
actions.onGlobalStateChange((state, prev) => {})
actions.setGlobalState({ token: 'xxx' })

// 子应用
export async function mount(props) {
  props.onGlobalStateChange((state, prev) => {}, true)
  props.setGlobalState({ token: 'xxx' })
}
// 主应用
import { initGlobalState } from 'qiankun'
const actions = initGlobalState({ token: '' })
actions.onGlobalStateChange((state, prev) => {})
actions.setGlobalState({ token: 'xxx' })

// 子应用
export async function mount(props) {
  props.onGlobalStateChange((state, prev) => {}, true)
  props.setGlobalState({ token: 'xxx' })
}

常见问题 ​

1. 样式隔离 ​

  • experimentalStyleIsolation: true — 添加 div[data-qiankun-xxx] 前缀
  • 子应用使用 CSS Modules / Scoped

2. 全局变量污染 ​

  • 沙箱模式下自动处理 window 对象
  • 注意第三方库的全局变量

3. 静态资源路径 ​

  • 子应用设置 publicPath 为完整 URL
  • 使用 webpack 的 __webpack_public_path__