@rokid-library/micro-app
TypeScript icon, indicating that this package has built-in type declarations

0.2.6 • Public • Published

@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

Readme

Keywords

none

Package Sidebar

Install

npm i @rokid-library/micro-app

Weekly Downloads

0

Version

0.2.6

License

MIT

Unpacked Size

309 kB

Total Files

10

Last publish

Collaborators

  • crisp.123
  • leonwayteam
  • yekangkang