引言
公司的 erp 项目已经很多年,为了方便维护和迭代,同时能保证不能模块之间能独立部署,独立运行,经过一番调研,决定使用微前端的思想将代码拆分。最开始使用 webpack5 的Module Federation
进行拆分,能保证每个模块独立部署,独立发布,每个子应用的 CI/CD 已经跑通,发布到开发环境也没问题。但是Module Federation
不同技术会有一定限制,有些极端情况没办法处理,同时 js 沙箱和 css 沙箱也是一定的问题,故决定使用qiankun
进行拆分。
本教程使用Vue
作为主应用的基座,接入不同技术栈的子应用,同时会拿一个模块来讲解通信相关内容。
准备工作
这里使用 vue 作为基座,因此首先需要创建 vue 教授架,使用vue-cli创建,命令如下:
- 安装 vue 脚手架
安装
yarn global add @vue/cli
检查版本是否正确
vue --version
创建项目
vue create hello-world
安装 qiankun
yarn add qiankun
- 安装 react 脚手架
这儿使用create-react-app作为 react 脚手架生成工具,具体操作如下:
安装
npx create-react-app my-app
cd my-app
yarn eject
yarn start
ERP 前端架构设计
erp 前端架构设计如下:
- 主应用(基座):用于注册子应用的容器,使用 vue 作为主应用,包括:登录、注销、修改密码、Layout、动态路由、公共 State 等;
- 子应用(若干):可使用任何技术栈;
- 公共模块: 两种方式(mf 或 npm 包) 3.1. Module Federation: 公共组件、指令、字典、工具方法存放于主应用,通过 mf shared 出去(推荐); 3.2. npm 包:将公共组件、指令、工具方法封装成 npm 包,更新版本所有应用都需要更新,稍微麻烦;
详细的设计如下图所示:
路由设计
erp 项目的路由分为主路由和子路由,主路由主要是登录、home 页、修改密码,子路由是各个模块自己的路由模块,具体如下
主路由
- 所有页面访问,都需要经过全局路由守卫,这时需判断该用户是否登录,如果没登录,跳转回登录页面;
- 登录页登录后拿到菜单权限,根据已经配置的所有模块的路由进行过滤,配置动态路由;
- 然后判断是否是启动页,如果不是跳转到 home 页, 如果是走当前启动页的子应用路由
子路由
- 子应用拿到主应用返回的菜单权限,配置动态路由
- 当主应用确定启动项后,会到当前启动项的子应用去找匹配的路由
通过上面的路由设计,即可完成主子应用的路由匹配情况,具体如下图所示
搭建主应用基座
创建好脚手架后,根据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 说明:
- registerMicroApps(apps, lifeCycles?): 注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。 1.1. apps: 必选,微应用注册信息 2.2. lifeCycles: 声明周期
- start(opts?): 启动 qiankun, opts: 可选
- setDefaultMountApp(appLink): 设置主应用启动后默认进入的微应用
- loadMicroApp(app, configuration?): 手动加载一个微应用
- prefetchApps(apps, importEntryOpts?): 手动预加载指定的微应用静态资源
- initGlobalState(state): 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
- setGlobalState(state): 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
- onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void: 监听状态的变化
- offGlobalStateChange() => boolean: 移除当前应用的状态监听,微应用 umount 时会默认调用
这儿只简单列了部分 api,详情请移步qiankun 官网
搭建 vue 微应用
使用 vue-cli 创建一个 vue 脚手架,安装好所需依赖
- 在 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 必须设置跨域,不然主应用拿不到微应用信息。
- 在 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'
- 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
}
- 主应用注册新增的 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 配置,然后进行下面的配置
- 在
config/webpack.config.js
配置如下:
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
- 在
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__
}
- 将新创建的
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'))
}
- 在主应用注册 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,如下
- initGlobalState(state): 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
- setGlobalState(state): 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
- onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void: 监听状态的变化
- offGlobalStateChange() => boolean: 移除当前应用的状态监听,微应用 umount 时会默认调用
首先说一下对于简单的主子通信,使用 qiankun 给我 api 完全够了
- 在主应用定义全局状态,如下:
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
- 子应用拿到主应用的状态
主应用在注册微应用时,通过 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)
}
当设置了全局状态的值后,所有监听了全局状态变化的地方都会返回最新的状态,这样就实现了主子通信,子子通信的情况。
对于复杂的状态管理,逻辑如下图所示
到这里便使用 qiankun 实现了不同技术栈的整合。demo git 仓库