@rokid-library/micro-app
京东微前端方案(0.8.10)在直接应用到业务上时遇到的问题:
1、不支持vite,尤其是子应用
vite不支持qiankun、飞冰、京东的微前端, 虽然社区有对应的插件或者解决方案,但是使用后 微前端里的很多特性都将失效)
2、京东方案中 不管是基座应用的setData、setGlobalData 还是子应用的 setGlobalData、dispatch
都是全量重置之前的状态(data、globaldata) 为了保留set前的数据需要用户手动合并,例如
const { dispatch,getData } = window.microApp
dispatch(
{
...getData(),
...data
}
)
开发体验差容易出错
3、以往的项目经验中总结出需要全局共享状态,其中包含两部分:
3.1、基座应用维护的, 比如{ lang: 'zh-cn' } 3.2、子应用维护的, 比如{ dynamicRoutes:[ { path:'/project', name:'成都博物馆' }, { path:'/project/poiDetail', name:'铜器' } ] } 其中1的部分是只有基座应用有权利维护 并且是给到所有子应用使用的。但是京东方案里的子应用有setGlobalData的能力。 导致的结果就是: 1、任意子应用可能误修改到基座应用维护的全局状态 2、如果任意子应用出现问题,可能导致所有应用出现问题 所以不能直接使用globalData当作全局的状态
4、以往的项目经验总结出应用间出了共享状态 还需要互相 发布、接收消息。
但是如果京东方案里 data、globalData 如果当作数据使用就无法当作data使用
为了解决遗忘2、3、4的问题,产生了@rokid-library/micro-app(京东微前端方案二次封装),主要实现了:
通信协议(Message)
不管是 京东方案里的全局的globaldata 还是 基座应用与特定子应用的data 统统用以下类型定义
interface Message<T> {
type: T
payload?: any
}
属性 | 说明 | 是否必填 | 类型 | 默认值 |
---|---|---|---|---|
type | 当前这条消息的类型(含义) | 是 | enum | - |
payload | 当前消息携带的载荷 | 否 | any | - |
基础管理器(Manager)
import * as microAppModule from '@micro-zoe/micro-app';
import type { EventCenterForMicroApp, MicroApp } from '@micro-zoe/micro-app';
import type { OptionsType } from '@micro-app/types';
export { EventCenterForMicroApp } from '@micro-zoe/micro-app';
export type BaseManagerOptions = {
startOption: OptionsType;
};
export type ManagerType = 'base' | 'sub';
export type MicroAppType = MicroApp | EventCenterForMicroApp;
export declare class BaseManager {
microAppModule: typeof microAppModule;
type: 'base';
microApp: MicroApp;
/**
* @param type // 当前管理器类型
* @param microAppConfig // 当前管理器微前端实例
*/
constructor({ startOption }: BaseManagerOptions);
}
export declare class SubManager {
subName: string;
type: 'base';
microApp?: EventCenterForMicroApp;
}
消息(事件)管理器(EventManager<T,U>)
import { BaseManager, BaseManagerOptions, SubManager } from './manager';
export declare class BaseEventManager<T, U> extends BaseManager {
/**
*
* @param options
*/
constructor(options: BaseManagerOptions);
/**
* 订阅消息
* @param appName 基座应用订阅name为appName的子应用消息
* @param message 消息类型
* @param cb 消息的回调函数
* @returns
*/
subscribe(appName: string, message: T, cb: (payload: any) => void): () => void;
/**
* 订阅全局消息
* @param message 全局消息类型
* @param cb 全局消息回调函数
* @returns
*/
subscribeGlobal(message: U, cb: (payload: any) => void): () => void;
/**
* 清空所有订阅消息(不包含全局消息)
*/
clear(): void;
/**
* 清空所有全局订阅的消息
*/
clearGlobal(): void;
}
export declare class SubEventManager<T, U> extends SubManager {
/**
* 初始化的时候 传入microApp 代表是基座应用、不传代表是子应用
* @param microApp
*/
constructor();
/**
* 订阅消息
* @param message 消息类型
* @param cb 消息的回调函数
* @returns
*/
subscribe(message: T, cb: (payload: any) => void): () => void;
/**
* 订阅全局消息
* @param message 全局消息类型
* @param cb 全局消息回调函数
* @returns
*/
subscribeGlobal(message: U, cb: (payload: any) => void): () => void;
/**
* 清空所有订阅消息(不包含全局消息)
*/
clear(): void;
/**
* 清空所有全局订阅的消息
*/
clearGlobal(): void;
}
包含全局状态的消息管理器(GlobalStateEventManager)
import { BaseManagerOptions } from './manager';
import { BaseEventManager, SubEventManager } from './event-manager';
interface BaseOptions<G> extends BaseManagerOptions {
shouldUpdateGlobalStateBySubApp?: (data: UpdateBySubApp<G>) => boolean;
initGlobalState?: G;
}
interface UpdateBySubApp<G> {
nextData: Partial<G>;
currentData: G;
appName: string;
}
export declare enum EVENT_TYPES {
navigate = "navigate",
globalState = "global-state",
getGlobalState = "get-global-state",
setGlobalState = "set-global-state"
}
export declare enum GLOBAL_EVENT_TYPES {
subAppChange = "sub-app-change"
}
declare abstract class StateEventManager<G, T, U> {
/**
* 设置全局状态 GlobalState
* @param data
*/
setGlobalState: (data: Partial<G>) => void;
getGlobalState: () => G;
addGlobalStateChangeListener: (cb: (data: G) => void) => () => void;
destroy: () => void;
}
export declare class BaseStateEventManager<G extends Record<string | number, any> | undefined, T = EVENT_TYPES, U = GLOBAL_EVENT_TYPES> extends BaseEventManager<T | EVENT_TYPES, U | GLOBAL_EVENT_TYPES> implements StateEventManager<G, T, U> {
private globalState: G;
private subAppNames?;
private unSubscribes;
private globalStateChangeListeners;
shouldUpdateGlobalStateBySubApp?: (data: UpdateBySubApp<G>) => boolean;
/**
* 构造函数依次执行
* 1、初始化属性,初始化父类
* 2、将初始化 initGlobalState 同步到 globalState 并且通知给所有子应用
* 3、监听所有来自子应用获取全局状态的事件以及修改全局状态的事件
* 4、监听子应用已经更新事件(GLOBAL_EVENT_TYPES.subAppChange)
*
*/
constructor(options?: BaseOptions<G>);
/**
* 设置全局状态 GlobalState
* @param data
*/
setGlobalState: (data?: Partial<G>) => void;
addGlobalStateChangeListener: (cb: (data: G) => void) => () => void;
/**
* 获取全局状态 GlobalState
* @param data
*/
getGlobalState: () => G;
/**
* 更新 所有子应用相关的操作
* 当自应用数量发生变化的时候
* 给所有新增的子应用 添加 EVENT_TYPES.getGlobalState、EVENT_TYPES.setGlobalState这两个消息的订阅
* 同时 立即给所有的新的子应用下发一次(通过sendGlobalDataToSubApp) globalState
*/
private updateSubApps;
/**
* 修改子应用本地的全局状态
* 如果是基座应用 那么向所有子应用下发最新的 globalState
* @param data 完整的全局状态
*/
private setLocalGlobalState;
/**
* 获取子应用本地的全局状态
* @param data
* @returns 当前子应用本地的globalState
*/
private getLocalGlobalState;
/**
* 基座应用 收到来自子应用的修改全局状态的请求后 根据具体情况决定是否修改全局状态
* @param data
* @returns
*/
private setGlobalDataBySubApp;
/**
* 基座应用 向单子应用单独下发全局状态
* @param appName
*/
private sendGlobalDataToSubApp;
destroy: () => void;
}
export declare class SubStateEventManager<G extends Record<string | number, any> | undefined, T = EVENT_TYPES, U = GLOBAL_EVENT_TYPES> extends SubEventManager<T | EVENT_TYPES, U | GLOBAL_EVENT_TYPES> implements StateEventManager<G, T, U> {
globalState: G;
private unSubscribes;
private globalStateChangeListeners;
/**
* 构造函数依次执行
* 1、初始化属性,初始化父类
* 5、向基座应用通知一次 子应用已经更新(GLOBAL_EVENT_TYPES.subAppChange)
* 6、子应用监听 来自主应用下发的全局状态事件
* 7、向基座应用请求一次全局状态
*/
constructor();
/**
* 设置全局状态 GlobalState
* @param data
*/
setGlobalState: (data?: Partial<G>) => void;
/**
* 获取全局状态 GlobalState
* @param data
*/
getGlobalState: () => G;
addGlobalStateChangeListener: (cb: (data: G) => void) => () => void;
/**
* 修改子应用本地的全局状态
* 如果是基座应用 那么向所有子应用下发最新的 globalState
* @param data 完整的全局状态
*/
private setLocalGlobalState;
/**
* 获取子应用本地的全局状态
* @param data
* @returns 当前子应用本地的globalState
*/
private getLocalGlobalState;
/**
* 请求基座应用修改全局状态(GlobalState)
* @param data 请求修改的全局状态
*/
private requestSetGlobalState;
/**
* 主动向基座应用请求全局状态(GlobalState)
* 基座应用收到后会向该子应用单独下发全局状态(GlobalState) 通过事件(EVENT_TYPES.globalState)
*/
private requestGetGlobalState;
destroy: () => void;
}
export {};
使用demo
基座应用demo
/src/micro-app/global-state-event-manager.ts
import microApps from './apps'
import { BaseStateEventManager } from '@rokid-library/micro-app'
import { useEffect, useState } from 'react'
export interface GlobalState {
lang: string
name?: string
}
export enum SELF_EVENT_TYPES {}
export enum SELF_GLOBAL_EVENT_TYPES {}
export const globalEventManager = new BaseStateEventManager<
GlobalState,
SELF_EVENT_TYPES,
SELF_GLOBAL_EVENT_TYPES
>({
startOption: {
// destroy: true
// shadowDOM: true
preFetchApps: microApps
},
initGlobalState: {
lang: 'zh-cn'
}
})
export { EVENT_TYPES, GLOBAL_EVENT_TYPES } from '@rokid-library/micro-app'
export const setGlobalState = globalEventManager.setGlobalState
export const getGlobalState = globalEventManager.getGlobalState
export const useGlobalState = (): [
GlobalState,
(data?: Partial<GlobalState>) => void
] => {
const [data, setter] = useState(globalEventManager.globalState)
useEffect(() => {
const unListener = globalEventManager.addGlobalStateChangeListener(setter)
return unListener
}, [])
return [data, globalEventManager.setGlobalState]
}
src/views/home/index.tsx
import { Button } from 'antd'
import React from 'react'
import {
setGlobalState,
getGlobalState,
useGlobalState
} from '@/micro-app/global-event-manager'
import s from './index.module.less'
interface HomeProps {}
const Home: React.FC<HomeProps> = () => {
const [globalData, setGlobal] = useGlobalState()
const set = () => {
setGlobalState({
name: '123'
})
}
const set2 = () => {
setGlobal({
name: '1234'
})
}
const get = () => {
const data = getGlobalState()
console.log('getGlobalState', data)
}
return (
<div className={s['home-root']}>
我是首页
<div>全局状态:{JSON.stringify(globalData)}</div>
<Button onClick={get}>获取globalstate</Button>
<Button onClick={set}>修改globalstate</Button>
<Button onClick={set2}>通过hooks修改globalstate</Button>
</div>
)
}
export default Home
src/modules/menu/index.tsx
import React from 'react'
import { Menu } from 'antd'
import type { MenuItemProps } from 'antd/es/menu'
import { useNavigate, useMatches } from 'react-router-dom'
import microAppConfigs from '@/micro-app/apps'
import { globalEventManager } from '@/micro-app/global-event-manager'
import { menus } from '@/config/menu'
import s from './index.module.less'
import { EVENT_TYPES } from '@rokid-library/micro-app'
interface SideMenuProps {}
const SideMenu: React.FC<SideMenuProps> = () => {
const navigate = useNavigate()
const onClick: MenuItemProps['onClick'] = (data) => {
const { key, item } = data
const microAppConfig = microAppConfigs.find(({ baseroute }) =>
key.startsWith(baseroute)
)
// const menuItem = menus.find(({ key }) => key === key)
// console.log('menuItem', menuItem, key)
if ((item as any).props.blank) {
window.open(key)
return
}
if (microAppConfig) {
globalEventManager.microApp.setData(microAppConfig.name, {
type: EVENT_TYPES.navigate,
payload: key
})
}
// console.log('getActiveApps main', getActiveApps())
// console.log('data', data)
navigate(key)
}
const matchPaths = useMatches()
return (
<div className={s['side-menu-root']}>
我是菜单
<Menu
style={{ width: 200 }}
mode="inline"
items={menus}
onClick={onClick}
selectedKeys={matchPaths.map((v) => v.pathname)}
/>
</div>
)
}
export default SideMenu
子应用demo
src/micro-app/event-manager.ts
import { EVENT_TYPES, SubStateEventManager } from '@rokid-library/micro-app'
import { useEffect, useState } from 'react';
export { EVENT_TYPES, GLOBAL_EVENT_TYPES } from '@rokid-library/micro-app'
export interface GlobalState {
lang: string
name?: string
subProp?: string
addType?: string
}
export enum SUB_EVENT_TYPES {
cancelMediaUpload = 'cancel-upload-media', // 取消上传媒资的消息
successMediaUpload = 'success-upload-media', // 上传媒资成功的消息
uploadMediaSource = 'upload-media-source' // 向媒资中心传递当前上传媒资所需要的配置
}
export const eventManager = new SubStateEventManager<
GlobalState,
SUB_EVENT_TYPES
>()
export const { setGlobalState } = eventManager
export const { getGlobalState } = eventManager
export const useGlobalState = () => {
const [data, setter] = useState(eventManager.globalState)
useEffect(() => {
const unSubscribe = eventManager.subscribe(EVENT_TYPES.globalState, setter)
return unSubscribe
}, [])
return [data, eventManager.setGlobalState]
};
src/router.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { eventManager, EVENT_TYPES } from '@/micro-app/event-manager'
import routConfig from '@/config/route'
import { Loading } from '@/components'
const router = createBrowserRouter(routConfig, {
basename: (window as any).__MICRO_APP_BASE_ROUTE__ || '/',
})
eventManager.subscribe(EVENT_TYPES.navigate, router.navigate)
const Routes = () => <RouterProvider router={router} fallbackElement={<Loading />} />
export default Routes
src/pages/home/index.tsx
import React, { useCallback, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useModelState } from '@/store'
import {
setGlobalState, getGlobalState, useGlobalState, eventManager,
} from '@/micro-app/event-manager'
import { Button, Modal, Select } from 'antd'
import request from '@/utils/request'
import img from '@/assets/images/favicon.png'
import s from './index.module.less'
interface HomeProps {}
const options = [
{
label: '123',
value: 123,
},
{
label: '111',
value: 111,
},
]
const appName = process.env.SUB_NAME
const Home: React.FC<HomeProps> = () => {
const navigate = useNavigate()
const [globalData, setGlobal] = useGlobalState()
const set = () => {
setGlobalState({
name: '123',
})
}
const set2 = () => {
setGlobal({
name: '1234',
})
}
const get = () => {
const data = getGlobalState()
console.log('getGlobalState', data)
}
return (
<div className={s['home-root']}>
我是
{appName}
home
<div>
全局状态:
{JSON.stringify(globalData)}
</div>
<Button onClick={get}>获取globalstate</Button>
<Button onClick={set}>修改globalstate</Button>
<Button onClick={set2}>通过hooks修改globalstate</Button>
<Button onClick={() => {
eventManager.microApp.setGlobalData({ dad: '123321123' })
}}
>
修改globalData
</Button>
<Button
type="primary"
onClick={() => {
navigate('/add-source?addType=1')
}}
>
上传平面视频
</Button>
</div>
)
};
export default Home