module federation如何在主应用与子应用共享状态
微前端中常见的问题就是状态共享,比如主应用登陆了,跳到子应用能够共享用户状态
这块跳转就分俩种情况:
- 在主应用中引入了子应用,跳转到主应用中引入的这个子应用(
module federation的话相当于主应用引入了子应用这个模块,给这个模块配个路由,跳转到这个路由),这块可能会想到既然成了主应用的一个模块了,直接用主应用的store就行了,但是子应用也是要单独部署的也有自己的store,这块要做到对子应用的侵入最小化 - 直接跳转到单独部署的子应用
这里主要讲的是第一种情况下的状态共享,至于第二种情况目前想到的只有路由传参数或者二次验证
例子
先来看一个例子,看一下状态共享主要在哪块进行处理,之后再分不同业务下的处理方案
每个应用都有num和user存储在store
子应用app2的配置和组件如下
`配置`
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
},
exposes: {
'./app': './src/app',
'./index': './src/pages/index'
},
shared: {
react: { singleton: true },
"react-dom": {
singleton: true,
}
},
})
`App组件`
import React from "react";
import { Provider } from 'react-redux'
import store from "./store";
import Index from './pages/index'
const App = () => {
return (
<Provider store={store}>
<Index />
</Provider>
)
}
export default App;
`Index组件`
import { useSelector, useDispatch } from 'react-redux'
import { add } from '../store/num'
import { change } from 'app1/user'
export default () => {
let dispatch = useDispatch()
let count = useSelector(state => state.num.count)
let user = useSelector(state => state.user)
let changeCount = () => {
dispatch(add())
}
let changeUser = () => {
dispatch(change({name: user.name, age: user.age + 1}))
}
return (
<>
app2 count: {count}
<p>{user.name}</p>
<p>{user.age}</p>
<button onClick={changeCount}>change app2 count</button>
<button onClick={changeUser}>change app2 user</button>
</>
)
}
app1主应用配置和组件
因为app1是主应用,所以要多个路由配置来做跳转
`配置`
new ModuleFederationPlugin({
name: "app1",
filename: 'remoteEntry.js',
remotes: {
app2: "app2@http://localhost:3002/remoteEntry.js",
},
exposes: {
'./user': './src/store/user'
},
shared: {
react: {singleton: true},
"react-dom": {
singleton: true,
}
}
})
`App组件`
import React from "react";
import { Provider } from 'react-redux'
import { RouterProvider } from 'react-router-dom'
import store from "./store";
import router from "./router";
const App = () => {
return (
<Provider store = {store}>
<RouterProvider router={router}/>
</Provider>
)
}
export default App;
`router配置`
import { createBrowserRouter } from 'react-router-dom'
import Layout from './components/layout'
import User from './pages/user'
import App2 from 'app2/app'
import App2Index from 'app2/index'
let router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <User />
},
{
path: 'app2',
element: <App2 />
},
{
path: 'app2index',
element: <App2Index />
}
]
}
])
export default router
可以看到俩边都状态是互不相关的,是因为俩个app都提供了store实例,
<Provider store={store}>,provider就近原则状态没有共享。
所以处理好store就能够处理好状态共享,说白了就是单例和多例的区别
仅使用app2的部分页面场景
如果只是使用了app2的index页面,可以把index页面使用到的store的数据保证在app1也存在即可,上面的代码中把app1的user reducer做了module federation配置导出,在app2中做了导入,同时俩个app的store也各自维护了一个num reducer,这俩种都能共享,只不过是代码复用上的差别,或者某个应用的num有一些额外的逻辑
导入app2整个应用
如果导入整个应用也用上面那种方法的话,那么app1需要有app2的所有reducer,侵入还是比较大的
这块还是有俩种情况:
- 主应用的数据只是用来做子应用数据的初始化,即单向的,只能主应用去修改
- 主应用和子应用数据是双向的
数据做初始化
针对第一种情况可以通过props注入的方式 修改主应用router
import { createBrowserRouter } from 'react-router-dom'
import Layout from './components/layout'
import User from './pages/user'
import App2 from 'app2/app'
import App2Index from 'app2/index'
import { useSelector } from 'react-redux'
let router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <User />
},
{
// 将主应用的数据注入到app2
path: 'app2',
// element: <App2 />
Component: () => {
// let count = useSelector(state => state.num.count)
// let user = useSelector(state => state.user)
return <App2/>
}
},
{
path: 'app2index',
element: <App2Index />
}
]
}
])
export default router
修改app2,看一下接收到的数据
import { Provider } from 'react-redux'
import store from "./store";
import Index from './pages/index'
const App = (props) => {
console.log(props)
return (
<Provider store={store}>
<Index />
</Provider>
)
}
export default App;
数据双向
因为是俩个store,所以实现双向也就是把俩个store联系起来就可以 首先修改子应用app2的配置,让他把store实例导出
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
},
exposes: {
'./app': './src/app',
'./index': './src/pages/index',
'./store': './src/store/index'
},
shared: {
react: { singleton: true },
"react-dom": {
singleton: true,
}
},
}),
然后修改router,在app2组件初始化的时候进行store关联
import { createBrowserRouter } from 'react-router-dom'
import Layout from './components/layout'
import User from './pages/user'
import App2 from 'app2/app'
import App2Index from 'app2/index'
import { useSelector } from 'react-redux'
import app2Store from 'app2/store'
import app1Store from './store/index'
import {change} from './store/user'
import { useEffect } from 'react'
let router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <User />
},
{
path: 'app2',
// element: <App2 />
Component: () => {
// let count = useSelector(state => state.num.count)
// let user = useSelector(state => state.user)
useEffect(() => {
let unapp1sub = app1Store.subscribe(() => {
let currentState = app1Store.getState()
let app2State = app2Store.getState()
if(currentState.user.age != app2State.user.age) {
app2Store.dispatch(change({name: currentState.user.name, age: currentState.user.age}))
}
})
let unapp2sub = app2Store.subscribe(() => {
let currentState = app2Store.getState()
let app1State = app1Store.getState()
if(currentState.user.age !== app1State.user.age) {
app1Store.dispatch(change(currentState.user))
}
})
return () => {
unapp1sub()
unapp2sub()
}
}, [])
return <App2/>
}
},
{
path: 'app2index',
element: <App2Index />
}
]
}
])
export default router
这里只是简单的对user.age做了一个比较然后改变的时候进行同步
无论是app1还是app2触发user.age改动的时候,都会让俩边数据同步
代码仓库: gitee.com/summarize/m…
总结
跳转子应用俩种方式:
- 主应用内部访问子应用
- 数据单向,只是主应用进行更改数据,可以使用props注入的方式
- 数据双向,无论主应用还是子应用都能进行修改,关联store
- 访问单独的子应用
- 路由传参或者二次验证