基于qiankun的微前端

1,403 阅读7分钟

引言

公司的 erp 项目已经很多年,为了方便维护和迭代,同时能保证不能模块之间能独立部署,独立运行,经过一番调研,决定使用微前端的思想将代码拆分。最开始使用 webpack5 的Module Federation进行拆分,能保证每个模块独立部署,独立发布,每个子应用的 CI/CD 已经跑通,发布到开发环境也没问题。但是Module Federation不同技术会有一定限制,有些极端情况没办法处理,同时 js 沙箱和 css 沙箱也是一定的问题,故决定使用qiankun进行拆分。

本教程使用Vue作为主应用的基座,接入不同技术栈的子应用,同时会拿一个模块来讲解通信相关内容。

main.jpeg

准备工作

这里使用 vue 作为基座,因此首先需要创建 vue 教授架,使用vue-cli创建,命令如下:

  1. 安装 vue 脚手架

安装

yarn global add @vue/cli

检查版本是否正确

vue --version

创建项目

vue create hello-world

安装 qiankun

yarn add qiankun
  1. 安装 react 脚手架

这儿使用create-react-app作为 react 脚手架生成工具,具体操作如下:

安装

npx create-react-app my-app
cd my-app
yarn eject
yarn start

ERP 前端架构设计

erp 前端架构设计如下:

  1. 主应用(基座):用于注册子应用的容器,使用 vue 作为主应用,包括:登录、注销、修改密码、Layout、动态路由、公共 State 等;
  2. 子应用(若干):可使用任何技术栈;
  3. 公共模块: 两种方式(mf 或 npm 包) 3.1. Module Federation: 公共组件、指令、字典、工具方法存放于主应用,通过 mf shared 出去(推荐); 3.2. npm 包:将公共组件、指令、工具方法封装成 npm 包,更新版本所有应用都需要更新,稍微麻烦;

详细的设计如下图所示:

erp-frontend-desigin.png

路由设计

erp 项目的路由分为主路由和子路由,主路由主要是登录、home 页、修改密码,子路由是各个模块自己的路由模块,具体如下

主路由

  1. 所有页面访问,都需要经过全局路由守卫,这时需判断该用户是否登录,如果没登录,跳转回登录页面;
  2. 登录页登录后拿到菜单权限,根据已经配置的所有模块的路由进行过滤,配置动态路由;
  3. 然后判断是否是启动页,如果不是跳转到 home 页, 如果是走当前启动页的子应用路由

子路由

  1. 子应用拿到主应用返回的菜单权限,配置动态路由
  2. 当主应用确定启动项后,会到当前启动项的子应用去找匹配的路由

通过上面的路由设计,即可完成主子应用的路由匹配情况,具体如下图所示

router_design.png

搭建主应用基座

创建好脚手架后,根据qiankun官网教程,改造主应用基座,首先需要在入口文件注册微应用信息,创建微应用容器,设置默认路由等,并启动

在主应用的入口文件main.js注册微应用信息,启动,代码如下:

//  main.js文件
// .......

// 注册微应用信息
registerMicroApps(
  [
    {
      name: 'sub-vue',
      entry: '//localhost:7001',
      container: '#subapp-viewport',
      activeRule: '/sub-vue',
      props: {
        shared,
      },
    },
  ],
  {
    beforeLoad: [
      (app) => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name)
      },
    ],
    beforeMount: [
      (app) => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
      },
    ],
    afterUnmount: [
      (app) => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name)
      },
    ],
  }
)

// 设置默认路由
setDefaultMountApp('/sub-vue')
// 启动,并开启严格沙箱模式
start({ experimentalStyleIsolation: true })

// ...  App.vue 文件
// 设置子应用容器
<div id="subapp-viewport"></div>

qiankun API 说明:

  1. registerMicroApps(apps, lifeCycles?): 注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。 1.1. apps: 必选,微应用注册信息 2.2. lifeCycles: 声明周期
  2. start(opts?): 启动 qiankun, opts: 可选
  3. setDefaultMountApp(appLink): 设置主应用启动后默认进入的微应用
  4. loadMicroApp(app, configuration?): 手动加载一个微应用
  5. prefetchApps(apps, importEntryOpts?): 手动预加载指定的微应用静态资源
  6. initGlobalState(state): 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
  7. setGlobalState(state): 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
  8. onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void: 监听状态的变化
  9. offGlobalStateChange() => boolean: 移除当前应用的状态监听,微应用 umount 时会默认调用

这儿只简单列了部分 api,详情请移步qiankun 官网

搭建 vue 微应用

使用 vue-cli 创建一个 vue 脚手架,安装好所需依赖

  1. 在 vue.config.js 中配置如下:
const { name } = require('../package.json')

module.exports = {
  // publicPath: '/subapp/sub-vue',
  chainWebpack: (config) => config.resolve.symlinks(false),
  configureWebpack: {
    output: {
      // 把子应用打包成 umd 库格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
  devServer: {
    port: 7001,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
}

webpack中的libraryTarget设置为umd或者window,表示 library 下的所有模块暴露到了全局,主应用就可以通过微应用的生命周期钩子获取。devServer 中的 headers 必须设置跨域,不然主应用拿不到微应用信息。

  1. 在 src 下创建一个 public-path.js 文件,加上下面代码,并在 main.js 引入(必须在最开头引入)
// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

// main.js
import './public-path'
  1. main.js 配置声明周期,如下
import './public-path'
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'
import VueRouter from 'vue-router'
import actions from './shared/actions'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.config.productionTip = false
let instance = null

Vue.use(ElementUI)

function render(props = {}) {
  if (props) {
    // 注入 actions 实例
    actions.setActions(props)
  }

  const { container } = props
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/sub-vue' : '/',
    mode: 'history',
    routes,
  })

  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app')
}

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

export async function bootstrap() {
  console.log('[vue] vue app bootstraped')
}

export async function mount(props) {
  console.log('[vue] props from main framework', props)

  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log('子应用监听的全局状态', state)
  })

  // 注册子应用路由
  props.setGlobalState({ routes })

  render(props)
}

export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
}
  1. 主应用注册新增的 vue 应用,入口 main.js 文件中
registerMicroApps(
  [
    {
      name: 'sub-vue',
      entry: '//localhost:7001',
      container: '#subapp-viewport',
      activeRule: '/sub-vue',
      props: {
        shared,
      },
    },
  ],

到这里,Vue 微应用就配置好了,此时启动主子应用就可以在主应用中看到刚接入的子应用信息。

搭建 React 微应用

按照上面的教程,使用create-react-app创建一个 react 脚手架,使用yarn eject命令暴露出 webpack 配置,然后进行下面的配置

  1. config/webpack.config.js配置如下:
output: {
      // 把子应用打包成 umd 库格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  1. config/webpackDevServer.config.js文件中配置 headers 跨域,如下:
    headers: {
      'Access-Control-Allow-Origin': '*',
    },

在 src 下创建文件public-path.js文件,内容如下:

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
  1. 将新创建的public-path.js文件,在入口文件src/index.js文件的最开头引入,并配置qiankun的生命周期,如下:
import './public-path'
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import 'antd/dist/antd.css'
import actions from './shared/actions'

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

function render(props = {}) {
  if (props) {
    // 注入 actions 实例
    actions.setActions(props)
  }
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  )
}

export async function bootstrap() {
  console.log('react app bootstraped')
}

export async function mount(props) {
  console.log('reactApp mount', props)
  render(props)
}

export async function unmount() {
  console.log('react unmount')
  ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
  1. 在主应用注册 react 应用即可,如下
// 注册微应用信息
registerMicroApps([
  {
    name: 'sub-vue',
    entry: '//localhost:7001',
    container: '#subapp-viewport',
    activeRule: '/sub-vue',
    props: {
      shared,
    },
  },
  {
    name: 'sub-react',
    entry: '//localhost:7002',
    container: '#subapp-viewport',
    activeRule: '/sub-react',
    props: {
      shared,
    },
  },
])

到此,React 微应用已经搭建完成,可以启动看效果了~

主子通信管理

对于主子应用的通信,qiankun 给出有 api,如下

  1. initGlobalState(state): 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
  2. setGlobalState(state): 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
  3. onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void: 监听状态的变化
  4. offGlobalStateChange() => boolean: 移除当前应用的状态监听,微应用 umount 时会默认调用

首先说一下对于简单的主子通信,使用 qiankun 给我 api 完全够了

  1. 在主应用定义全局状态,如下:
import { initGlobalState } from 'qiankun'

const initialState = { routes: [] }
const actions = initGlobalState(initialState)

export default actions

初始化全局状态后,可使用上面定义的actions.onGlobalStateChange监听状态,实现原理就是观察者模式,具体操作如下:

actions.onGlobalStateChange((state, prev) => {
  console.log('新的状态:', state)
  console.log('上一次的状态:', state)
  store.commit('SET_ROUTES', state.routes)
})

拿到状态值改变后,可将值存放到当前使用技术的状态管理里面,这儿使用的是 vuex

  1. 子应用拿到主应用的状态

主应用在注册微应用时,通过 props 将全局状态传给子应用,子应用可以通过 props 拿到的的actions进行数据处理,也可通过actions.setGlobalState将子应用的数据返给主应用,如下

export async function mount(props) {
  console.log('[vue] props from main framework', props)

  props.onGlobalStateChange((state) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log('子应用监听的全局状态', state)
  })

  // 注册子应用路由
  props.setGlobalState({ routes })

  render(props)
}

当设置了全局状态的值后,所有监听了全局状态变化的地方都会返回最新的状态,这样就实现了主子通信,子子通信的情况。

对于复杂的状态管理,逻辑如下图所示

store.png

到这里便使用 qiankun 实现了不同技术栈的整合。demo git 仓库

参考文档

  1. vue-cli
  2. create-react-app
  3. qiankun