前言
前面一篇文章Single-spa 成熟的微前端架构方案介绍了要用微前端架构的背景和目的,简单对比了微前端架构方案的技术选型,以及分享了一些微前端的概念图和简单的架构图。
这篇文章属于项目实战,利用qiankun(一个基于 single-spa 的微前端实现库)加Ant Design组件库实现微前端主应用和微应用的路由交互、状态通信等,并且微应用按需加载组件库,微应用支持独立运行模式。
主应用构建
从大部分业务需求上来看,主应用里面需要包括以下能力
- 网站头部信息或头部导航菜单
- 网站左侧菜单功能
- 系统登录页面和页面权限信息分发
- 能接入不同前端框架的微应用(Vue、React)
创建主应用
微前端解决方案,我们选择qiankun(一个基于 single-spa 的微前端实现库), 系统UI我们选用一个开箱即用的中台前端设计解决方案 Vue Antd Admin
1
| git clone --depth=1 https://github.com/iczer/vue-antd-admin.git
|
admin项目拉下来安装依赖
1 2 3
| yarn # or npm install
yarn add qiankun # 或者 npm i qiankun -S
|
主应用qiankun注册微应用逻辑
安装qiankun依赖后在src目录下面新增microAppRegister.js文件,用户注册微应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
| import { registerMicroApps, start, setDefaultMountApp, runAfterFirstMounted, initGlobalState } from 'qiankun'
export default function microAppRegister (vm) { if(!window.isQiankunStart) { window.isQiankunStart = true
let langInfo = vm.$store.state.setting.lang
const mainLifeCycles = { 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) } ], afterMount : [ app => { console.log('[LifeCycle] after mount %c%s', 'color: red;', app.name) let newLangInfo = vm.$store.state.setting.lang if(newLangInfo !== langInfo) { langInfo = newLangInfo actions.setGlobalState({ langInfo: newLangInfo }) } } ], afterUnmount: [ app => { console.log( '[LifeCycle] after unmount %c%s', 'color: green;', app.name ) } ] }
const loader = loading => { console.log('LOADER %c%s', 'color:yellow', loading) vm.$store.commit('microApp/loadingToggle', loading) } let apps = vm.$store.getters['microApp/microApps'] let userInfo = vm.$store.getters['account/user'] console.log('registerMicroApps', apps) registerMicroApps( apps.map(app => { let appInfo = {...app} appInfo.props = Object.assign({}, appInfo.props || {}, { userInfo: userInfo, langInfo: langInfo }) return { ...appInfo, loader } }), mainLifeCycles )
setDefaultMountApp(apps[0].activeRule)
start({ prefetch: true, singular: true })
runAfterFirstMounted(() => { console.log('[MainApp] first app mounted') }) } }
export const state = { testAttr: 'Hi Channing', scrollToBottom: false, microAppsRouterMap: [], isLoadingMicro: false }
console.log('initGlobalState...')
export const actions = initGlobalState(state)
export const { onGlobalStateChange, setGlobalState, offGlobalStateChange } = actions
actions.onGlobalStateChange((state, prev) => { console.log('主应用全局监听到state发生变化!!!!!!!!!!!!!!!!!!!!!!!!!!') console.log('state', state) console.log('prev', prev) })
actions.getGlobalState = (key) => { return key ? state[key] : state }
|
在src/store/modules下面添加microApp.js,用户管理微应用参数状态,动态加载微应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default { namespaced: true, state: { apps: [] }, getters: { }, mutations: { setApps (state, apps) { state.apps = apps } } }
|
修改src/config/config.js, 新增配置 asyncRoutes: true
设置路由为异步路由,因为我们需要用异步动态路由来控制权限。
主应用动态路由配置
需要修改部分模拟数据和路由注册逻辑,兼容一级目录为微应用入口的逻辑
- 把mock数据的root.children的一级目录添加部分属性,如
appName: 'micro-vue'
,path: 'micro-vue'
,entry: 'http://localhost:9000'
,这些属性是为了注册微应用用的。
- 在src/store/modules下面新增microApp.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export default { namespaced: true, state: { isLoading: false, apps: [] }, getters: { microApps: state => { return state.apps } }, mutations: { loadingToggle: (state, loading) => { state.isLoading = loading console.log("loadingToggle~~~~~~~~~~~~~~~~~~",loading) }, setApps (state, apps) { state.apps = apps } } }
|
- 修改src/utils/routerUtil.js里面的路由加载逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let apps = rootRouterConfig.map(item => { if (item.router === 'microApp') { console.log(item) return { name: item.appName, entry: item.entry, container: '#micro-page', activeRule: `/${item.path}`, $meta: { title: item.name }, props: { asyncRouterConfig: item.children } } } }) apps = apps.filter(res => res !== undefined) store.commit('microApp/setApps', apps)
|
1 2 3 4 5 6 7 8 9 10 11 12
| routes = routes.map(r => { if (r.path === '/') { r.children.map(rc => { if (rc.meta.microApp) { rc.children = [] rc.path = `${rc.path}/*` } }) } return r })
|
1 2 3 4 5 6 7
| const menuFinalRoutes = mergeRoutes(basicOptions.routes, allRoutes) formatRoutes(menuFinalRoutes) menuRouter = initRouter(store.state.setting.asyncRoutes) menuRouter.options = {...menuRouter.options, routes: menuFinalRoutes} menuRouter.matcher = new Router({...menuRouter.options, routes:[]}).matcher menuRouter.addRoutes(menuFinalRoutes)
|
微应用构建
微应用可以用Vue的Vue cli 或 React的Create React App
Vue微应用创建
Vue的微应用用vue-cli创建好项目后,可以参照qiankun的vue微应用实践文档
在适配Vue Antd Admin 的动态路由解析需要做一定的代码修改,可以参考我的实现demo
React微应用创建
可以用React官方的Create React App, 也可以尝试Vite构建项目,我前面有一篇文章介绍了Vite的魔力
在 src 目录新增 public-path.js
:
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。
1 2 3
| if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
|
设置 history
模式路由的 base:
1
| <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
|
- 入口文件
index.js
修改,为了避免根 id #root
与其他的 DOM 冲突,需要限制查找范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import './public-path'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App';
function render(props) { const { container } = props; ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root')); }
if (!window.__POWERED_BY_QIANKUN__) { render({}); }
export async function bootstrap() { console.log('[react16] react app bootstraped'); }
export async function mount(props) { console.log('[react16] props from main framework', props); render(props); }
export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root')); }
|
- 修改
webpack
配置
安装插件 @rescripts/cli
,当然也可以选择其他的插件,例如 react-app-rewired
。
根目录新增 .rescriptsrc.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const { name } = require('./package');
module.exports = { webpack: (config) => { config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = `webpackJsonp_${name}`; config.output.globalObject = 'window';
return config; },
devServer: (_) => { const config = _;
config.headers = { 'Access-Control-Allow-Origin': '*', }; config.historyApiFallback = true; config.hot = false; config.watchContentBase = false; config.liveReload = false;
return config; }, };
|
在 package.json
添加脚本:
1 2 3 4
| "dev": "rescripts start", "build:micro": "rescripts build", "test:micro": "rescripts test",
|
详细的API可以参考qiankun的官方文档
我的React的微应用demo
微应用接入主应用
首先微前端项目肯定是一个项目群,我的demo项目群地址:https://github.com/micro-antd-admin
这个群组里面有3个项目,一个基于Antd Admin集成qiankun的主应用,一个Vue微应用,一个React微应用。
更新demo项目后,运行两个微应用 npm run dev
会有两个本地服务端口。
然后去主应用的 src/mock/user/routes.js
模拟数据添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| { router: 'microApp', name: 'Vue 微应用', appName: 'micro-vue', path: 'micro-vue', entry: 'http://localhost:9000', icon: 'appstore', children: [ { router: 'parent', name: '二级目录', icon: 'ant-design', children: ['test'] }, { router: 'home', icon: 'calendar', name: '首页', }, { router: 'about', icon: 'bulb', name: '关于' } ] }, { router: 'microApp', name: 'React 微应用', appName: 'micro-react', path: 'micro-react', entry: 'http://localhost:3000', children: ['home', 'about', 'test'] }
|
以上demo如果有问题欢迎留言沟通😺