module federation如何在主应用与子应用共享状态

989 阅读5分钟

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;

props注入.png

数据双向

因为是俩个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改动的时候,都会让俩边数据同步

双向.png

代码仓库: gitee.com/summarize/m…

总结

跳转子应用俩种方式:

  • 主应用内部访问子应用
    • 数据单向,只是主应用进行更改数据,可以使用props注入的方式
    • 数据双向,无论主应用还是子应用都能进行修改,关联store
  • 访问单独的子应用
    • 路由传参或者二次验证