文章
问答
冒泡
在复杂应用下wujie的路由跳转解决方案
在比较复杂的前端应用中,微前端的应用场景也会相对比较复杂一点。比如tab页或者,同一个布局下的多视图切换。这里,我们还是以我们的工程为例。布局如下
 
我们的需求
  • 主应用跳转子应用
  • 子应用跳转主应用
  • 子应用跳转子应用
 
wujie现有的功能
根据官网的资料,wujie本身提供了路由同步和路由跳转的功能
由文档可知
只有无界实例在初次实例化的时候才会从url上读回路由信息,一旦实例化完成后续只会单向的将子应用路由同步到主应用url上
 
子应用在初始化的时候,才会去同步主应用路由上的信息,而初始化之后,主应用的路由信息,就对子应用没有影响了,这个时候,如果要改变子应用的路由,只能通过wujie的通信系统来改变活跃应用内的路由
 
 
解决方案
这里,我需要考虑子应用保活与不保活两种情况 ,以WujieReact为例
主应用跳转子应用
主应用跳转子应用,一个是主应用的路由,一个是设置子应用的路由,这里主要考虑子应用激活和未激活两种情况,如果是已激活,则无法控制子应用的路由,只能打开子应用,如果是未激活的情况,可以直接按照wujie的路由同步规则,打开子应用的对应路由页面。这里需同时通过路由同步和系统通信两种方式来兼容。
 
子应用跳转主应用
子应用跳转主应用的时候,子应用是没法控制主应用的路由的,这个时候可以借助wujie的props ,在主应用里设置一个回调函数,来控制主应用的路由。这里相对简单,主应的路由不需要同步。
子应用跳转子应用
子应用跳转子应用,应该是几个场景里最复杂的。例如子应用A 要跳转子应用B的页面,子应用A,先要把相关跳转的目标信息通过wujie的props中的回调函数发给主应用,主应用,再根据相关信息,跳转到子应用B ,并通过wujie系统通信控制子应用B的页面路由。
 
 
代码实现
主应用
这里我们使用保活模式,并开启路由同步
const degrade = window.localStorage.getItem("degrade") === "true" || !window.Proxy || !window.CustomElementRegistry;

const WuJieView = () => {
    const {state} = useLocation();
    const navigate = useNavigate()
    const {identifier} = useParams()
    const [activeApp, setActiveApp] = useState<any>()
    const [apps, setApps] = useState<{ key: string, name: string, url: string, route?: string }[]>([])
    const [appMiss, setAppMiss] = useState(false)

    const handleAddApp = (app: any) => {
        const appSnap = _.find(apps, (item) => {
            return item.name === app.identifier
        })

        if (appSnap) {
            appSnap.key = `${appSnap.name}-${randomString.generate(7)}`
            setApps(_.cloneDeep(apps))
        } else {
            setApps(_.concat(apps, {
                key: `${app.identifier}-${randomString.generate(7)}`,
                name: app.identifier,
                url: app.url,
                route: app.route
            }))
        }
        setActiveApp({name: app.identifier, url: app.url})

    }

    /**
    * 回调
    */
    const handleNavigate = ({app, url, route}: { app: string, url: string, route?: string }) => {
        navigate(url, {state: {route}})
        if (app) {
            WuJieReact.bus.$emit(`${app}:routeChange`, {route}) //通过wujie系统通信通知子应用变更路由
        }
    }

    useEffect(() => {
        if (state && state.url) {
            handleAddApp(state)
        } else {
            if (identifier) {
                appstoreApi.queryAppByIdentifier(identifier).then((app: any) => {
                    handleAddApp(app)
                }).catch(async (ex) => {
                    message.error(ex.message)
                })
            }

        }
    }, [state, identifier]);

    return <div style={{width: '100%', height: '100%'}}>
        {apps.map((appItem) => <div key={appItem.name}
                                    style={{
                                        width: '100%',
                                        height: '100%',
                                        display: (_.isEqual(appItem.name, activeApp.name) ? 'block' : 'none')
                                    }}>
            {_.isEqual(appItem.name, activeApp.name) && <WuJieReact
                height={"100%"}
                width={"100%"}
                name={appItem.name}
                url={appItem.url}
                sync={true}
                alive={true}
                degrade={degrade}
                props={{'route': appItem?.route, 'navigate': handleNavigate, "DubheToken": getTenantUserToken()}}  //此处通过navigate设置回调
                activated={() => {
                }}
                afterMount={(args) => {

                }}
                afterUnmount={(args) => {

                }}
            />}
        </div>)}
        {appMiss && <div>应用不存在</div>}
    </div>
}
export default WuJieView
 
如果是非保活模式,则需要在afterUnmount中调用销毁方法,否则再次打开的时候会失败
<WuJieReact
    height={"100%"}
    width={"100%"}
    name={appItem.name}
    url={appItem.url}
    sync={true}
    alive={false}
    degrade={degrade}
    props={{'route': appItem?.route, 'navigate': handleNavigate, "DubheToken": getTenantUserToken()}}
    activated={() => {}
    }
    afterMount={(args) => {   }}
    afterUnmount={(args)=>{
        WuJieReact.destroyApp(args.name)  //销毁应用
    }}
/>
 
在wujie 1.0.20版本下会有如下报错
sandbox.ts:372 Uncaught TypeError: Cannot read properties of null (reading '$clear')
这是因为,在afterUnmount 调用 destroyApp 的时候,函数中的 bus对象为null。 已经提了pr,不过不确定是否合并,如果合并了应该没这样的问题了。 https://github.com/Tencent/wujie/pull/742 如果没合并,或者没解决,可以自己拉源码修改
 
子应用代码
const wujieProps: any = _.get(window, ['$wujie', 'props'])

/**
* 监听路由变话通知
*/
useEffect(() => {
    const wujie: any = _.get(window, '$wujie')
    wujie?.bus?.$on(`${name}:routeChange`, ({route}: { route: string }) => {
        router?.navigate(route)
    })
}, []);

/**
* 初始化的时候,如果主应用带着route过来,则进行跳转
*/
useEffect(() => {
    if (wujieProps?.route) {
        router?.navigate(wujieProps?.route)
    }
}, [wujieProps?.route]);
 
跳转方式代码
export const navigateTo = ({app, url, route, navigate}: {
    app?: string,
    url: string,
    route?: string,
    navigate?: NavigateFunction
}) => {

    if (_.get(window, '__POWERED_BY_WUJIE__')) { //如果是wujie子应用,则调用主应用上的回调函数
        const props = _.get(window, ['$wujie', 'props'])
        if (!props?.navigate) {
            console.warn("[useDubheNavigate] missing navigate property")
        }
        props?.navigate?.({
            url: url,
            app,
            route: route
        })
    } else { //如果不是wujie子应用,则通过传入的navigate对象进行跳转
        navigate?.(url, {state: {route: url}})
    }
}

/**
* 拼接地址,根据实际情况处理
*/
export const hrefGenerate = ({to, app, hashRouter, appHashRouter}: {
    to?: string
    app?: string
    hashRouter?: boolean
    appHashRouter?: boolean
}) => {
    if (app) {
        let appHref = _.replace(`${appHashRouter ? '/#/' : '/'}${to}`, '//', '/')
        return _.replace(`${hashRouter ? '/#/' : '/'}/App/${app}`, '//', '/') + `?${app}=${encodeURIComponent(appHref)}`
    } else {
        return _.replace(`${hashRouter ? '/#/' : '/'}${to}`, '//', '/')
    }
}
 
综上
总之,基于wujie的微前端路由跳转,要根据wujie的功能特性,结合自己的业务进行综合处理,以期达到自己适用的解决方案。
wujie
微前端

关于作者

落雁沙
非典型码农
获得点赞
文章被阅读