主子应用状态管理
老项目(主应用)用到了 vuex 全局状态管理,所以新项目页面(子应用)里有时需要更改主应用里的状态,这里我用了 qiankun 的 globalState 来处理。
首先在 Container 里创建了 globalActions
,再监听 vuex 状态变更,每次变更都通知子应用,同时把 vuex 的 commit
和 dispatch
函数传给子应用:
import {initGlobalState, registerMicroApps, start} from 'qiankun' const globalActions = initGlobalState({ state: {}, commit: null, dispatch: null, }); export default { name: "Container", props: { visible: { type: Boolean, defaultValue: false, } }, mounted() { const { dispatch, commit, state } = this.$store; registerMicroApps([ { name: 'microReactApp', entry: '//localhost:3000', container: '#micro-app-container', activeRule: '/#/micro-react-app', // 初始化时就传入主应用的状态和 commit, dispatch props: { state, dispatch, commit, } }, ]) start() // vuex 的 store 变更后再次传入主应用的状态和 commit, dispatch this.$store.watch((state) => { console.log('state', state); globalActions.setGlobalState({ state, commit, dispatch }); }) }, } 复制代码
子应用里接收主应用传来的 state
,commit
以及 dispatch
函数,同时新起一个 Context,把这些东西都放到 MicroAppContext
里。(Redux 因为不支持存放函数这种 nonserializable 的值,所以只能先存到 Context 里)
// 渲染 function render(props: any) { const { container, state, commit, dispatch } = props; const value = { state, commit, dispatch }; const root = ( <HashRouter basename={basename}> <MicroAppContext.Provider value={value}> <App /> </MicroAppContext.Provider> </HashRouter> ); ReactDOM.render(root, container ? container.querySelector('#root') : document.querySelector('#root')); } // mount 时监听 globalState,只要一改再次渲染 App export async function mount(props: any) { console.log('[micro-react-app] mount', props); props.onGlobalStateChange((state: any) => { console.log('[micro-react-app] vuex 状态更新') render(state); }) render(props); } 复制代码
这样一来,子应用也可以通过 commit
,和 dispatch
来更改主应用的值了。
const OrderList: FC = () => { const { state, commit } = useContext(MicroAppContext); return ( <div> <h1 className="title">【微应用】订单列表</h1> <div> <p>主应用的 Counter: {state.counter}</p> <Button type="primary" onClick={() => commit('increment')}>【微应用】+1</Button> <Button danger onClick={() => commit('decrement')}>【微应用】-1</Button> </div> </div> ) }
当然了,这样的实践也是我自己 “发明” 的,不知道这是不是一个好的实践,我只能说这样能 Work。
全局变量报错
另一个问题就是当子应用隐式使用全局变量时,import-html-entry
执行 JS 时会直接爆炸。比如微应用有如下 <script>
的代码:
var x = {}; // 报错,要改成 window.x = {}; x.a = 1 // 报错,要改成 window.x.a = 1; function a() {} // 要改成 window.a = () => {} a() // 报错,要改成 window.a() 复制代码
在主应用加载微应用后,上面的 x
和 a
全都会报 xxx is undefined
,这是因为 qiankun 在加载微应用时,会执行这部分 JS 代码,而此时 var 声明的变量不再是全局变量,其他的文件无法获取到。
解决方法就是使用 window.xxx
来显式定义/使用全局变量。具体可见 Issue: 子应用全局变量 undefined
主应用切换路由时不更新子应用路由
只要主子应用都用上了 Hash 路由,那么很大概率会遇到这个问题。
比如你主应用有 /micro-app/home
和 /micro-app/user
两个路由,actvieRule
为 /#/micro-app
,子应用也有对应的 /micro-app/home
和 /micro-app/user
两个路由。
那么如果 在主应用里 从 /micro-app/home
切换到 /micro-app/user
,会发现子应用的路由并没有改变。但如果你 在主应用的子应用里 去切换,那么就能切换成功。
这是因为在主应用切换路由时不是通过 location.url
这种可以触发 hash change 事件的方式来变更路由,而 react-router 只监听了 hash change 事件,所以当主应用切换路由时,没有触发 hash change 事件,导致子应用的监听不到路由变化,也就不会做页面切换了。
具体可见:Issue: 加载子应用正常,但主应用切换路由,子应用不跳转,浏览器返回前进可触发子应用跳转。
解决方法很简单,下面三选一:
- 将 vue 主应用中的 Link 超链方式替换成原生的 a 标签,从而触发浏览器的 hash change 事件
- 主应用手动监听路由变更,同时手动触发 hash change 事件
- 主应用跟子应用都改用 browser history 模式
加载状态
主应用在加载子应用时还是需要不少时间的,所以最好要展示一个加载中的状态,qiankun 正好提供了一个 loader
回调来让我们控制子应用的加载状态:
<div class="container" :style="{ height: visible ? '100%' : 0 }"> <a-spin v-if="loading"></a-spin> <div id="micro-app-container"></div> </div> 复制代码
registerMicroApps([ { name: 'microReactApp', entry: '//localhost:3000', container: '#micro-app-container', activeRule: '/#/micro-react-app', props: { state, dispatch, commit, }, loader: (loading) => { this.loading = loading // 控制加载状态 } }, ]) start() 复制代码
总结
总的来说,微前端在解构巨石应用的帮助真的很大。像我们这种要重构整个应用的情况,部门肯定不会先暂停业务,给开发一整个月来专门重构的,只能在评新需求的时候多给你一两天时间而已。
微前端就可以解决重构的过程中边做新需求边重构的问题,使得新老页面都能共存,不会一下子整个业务都停掉来做重构工作。