Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

探索微前端的场景极限 #46

Open
kuitos opened this issue Mar 8, 2021 · 2 comments
Open

探索微前端的场景极限 #46

kuitos opened this issue Mar 8, 2021 · 2 comments

Comments

@kuitos
Copy link
Owner

kuitos commented Mar 8, 2021

本文主要介绍总结了一些基于 qiankun 的微前端应用场景与实践

基础场景

与路由绑定的方式渲染微应用

通常情况下,我们接触的最多的微前端的实践,是以 URL/路由 为维度来划分我们的微应用,以 OneX 平台(蚂蚁金融云基于微前端架构打造的统一接入平台)为例:

image.png

接入这类平台的微应用,通常只需要提供自己的 entry html 地址,并为其分配一个路由规则即可。

这背后的实现则是基于 qiankun 的 registerMicroApps API,如:

import { registerMicroApps } from 'qiankun';

registerMicroApps([
  {
    name: 'app1', 
    container: '#container', 
    entry: '//micro-app.alipay.com/', 
    activeRule: '/app1' 
  }
])

路由与应用绑定的方式简单直观,是微前端中最为常见的使用场景,通常我们会用这种方式将一堆独立域名访问的 MPA 应用,整合成一个一体化的 SPA 应用。

但这类场景也有自己的局限性:

  1. 由于URL/路由的 唯一性/排他性 的特点,这种方式只适用单实例场景需求
  2. 微应用的调度都是由路由系统来自动处理的,虽然省事但是碰到更复杂的需求,如同一个路由下,根据不同的用户权限展示不同的微应用这类个性化诉求,需要写一些中间层代码来曲线救国
  3. 应用挂载的容器节点等需提前准备好,不然碰到 动态/嵌套 路由这类情况,可能会因为路由 listener 监听触发的时序不确定,导致微应用无法完成挂载的问题

以组件的方式使用微应用

qiankun 2.0 的发布带来一个全新的 API loadMicroApp,通过这个 API 我们可以自己去控制一个微应用加载/卸载,这个方式也是 qiankun 2.0 的重磅特性:

import { loadMicroApp } from 'qiankun';

// do something

const container = document.createElement('div');
const microApp = loadMicroApp({ name: 'app', container, entry: '//micro-app.alipay.com' });

// do something and then unmount app
microApp.unmout();

// do something and then remount app
microApp.mount();

开发者可以在脱离路由的限制下,以更自由的方式去渲染我们的微应用。基于 loadMicroApp API,我们只需要做一些简单的封装,即可以类似组件的开发体验,完成微应用的接入,以 React 为例:

第一步:封装一个 MicroApp 组件:

import { loadMicroApp } from 'qiankun';
import React from 'react';

export default class MicroApp extends React.Component {

  containerRef = React.createRef();
  microApp = null;

  componentDidMount() {
    const { name, entry, ...props } = this.props;
    this.microApp = loadMicroApp(
      { name, entry, container: this.containerRef.current, props },
    );
  }

  componentWillUnmount() {
    this.microApp.unmount();
  }

  componentDidUpdate() {
    const { name, entry, ...props } = this.props;
    this.microApp.update(props);
  }

  render() {
    return <div ref={this.containerRef}></div>;
  }
}

第二步:通过 MicroApp 组件引入微应用:

import MicroApp from './MicroApp';
import React from 'react';

class App extends React.Component {
    render() {
    return (
      {
        this.props.admin
          ? <MicroApp name="admin" entry="//localhost:8080/" level={10} />
          : <MicroApp name="guest" entry="//localhost:8081/" level={1} />
      }
    )
  }
}

如果你是 umi 应用,那只需要直接使用插件封装好的组件即可:

import { MicroApp } from 'umi';

function MyComponent() {
  return (
    <div>
      <MicroApp name="qiankun" age={1.5} stars={8700} />
    </div>
  )
}

这类方式适用于一些可共用的、带业务逻辑的服务型组件(类似于我们以前常说的端对端组件):比如带聊天交互的客服机器人、带引导功能的 intro 服务等。

如蚂蚁 sofa 产品控制台中的这个使用入门微应用:

image

右侧呼出的窗口即为一个独立开发、独立发布的微应用

通过组件的这种方式,我们可以完全自主的控制微应用的渲染,并与之做一些复杂的交互。不论是在开发者的编码心智,还是用户的体验上,都跟使用一个普通的业务组件无异。

组件的方式非常灵活,几乎解决了所有路由绑定方式渲染微应用的问题,但也有自己的一些局限:比如我们会要求这类微应用必须是不带路由系统的 widget 类型,不然也会出现多实例时路由冲突的问题。

嵌套渲染场景

有一些更复杂的场景中,我们可能需要使用到「套娃」的方式集成我们的若干微应用。

比如我们要在应用 A 下集成应用 B 的商品列表页,然后在应用 B 的商品列表页呼出应用 C 的买家详情页,所有应用的唤起都以 弹层/抽屉 这种不刷新的交互来完成。

在有了上面两个场景的实践经验后,我们很容易得出这样的组合逻辑:

在应用 A 中通过调用 loadMicroApp(B) 的方式唤起微应用 B,然后在微应用 B 中通过 loadMicroApp(C) 的方式唤起微应用 C,通过这样的调用链路即可很完美的完成产品上的诉求。

但是现实情况往往没有那么简单,前面提到过,若想要 loadMicroApp API 能符合预期的运行,我们需要确保被加载的微应用是不含自己的路由系统,否则会出现多个应用间路由系统互相 抢占/冲突 的情况。

而现实情况是,我们大部分需要复用的页面/组件,都会是某个站点的局部路由页,很少有人会专门起一个仓库,用来专门把这个页面抽取成一个微应用,比如上面提到的买家详情页。

这种场景下,我们其实只需要确保微应用的路由系统不会干扰到全局的 URL 系统即可。幸运的是 react-router 的 memory history 模式很好的解决了这一问题。如果你是一个 umi 应用,只需要直接使用我们封装好的组件即可完成 memory history 的运行时切换:

import { MicroAppWithMemoHistory } from 'umi';

<Drawer>
  <MicroAppWithMemoHistory name="buyer" url='/buyers/123' />
</Drawer>

交互效果可以参考:

image

图中 app1/app2 均是子应用,但是在 app1 中可以再通过抽屉呼出 app2,同时浏览器地址栏也不会被 app2 的路由干扰。

关于嵌套渲染相关的详细介绍,可以看这篇《基于微前端的大型中台项目融合方案》

极限渲染场景

如果你觉得嵌套微应用就是我们场景的天花板了,那未免有点小看群众们的想象力了。

在我们内部的一个设计工程化平台里,我们通过 qiankun 成功的把一个微应用的 20+ 路由页同时渲染到了一个 url 下:

image.png

上图中右侧列表中每一个 demo 都是通过 qiankun 渲染的一个独立微应用实例,而这里面每一个微应用实例,实际对应是同一个 react 应用的不同路由页。

不同于前面几个场景,将同一个应用的不同页面,同时渲染到主应用的不同 UI 容器中这个需求下,有几个比较特殊的问题需要去考虑:

  1. 是否需要特殊的微应用生产方式
  2. 多路由系统共存带来的 冲突/抢占 问题
  3. 不同微应用间的样式隔离
  4. 如何优化渲染性能:既然每一个微应用实例实际对应的是同一个应用,那我们如何尽可能多的复用一些运行时或者沙箱,从而降低这么多微应用同时渲染代理的运行时开销

篇幅考虑,针对这类场景优化的技术细节不在这里介绍了,后面会单独写一篇来介绍

解决了这些问题后,我们只需要在我们的项目中这么去使用就可以了:

// MicroAppWithMemoHistory 是基于 memory history 封装的微应用加载器
import { MicroAppWithMemoHistory } from 'umi';

function Home {
  return (
    <div>
      { /* 将同一个应用的不同路由页同时渲染出来 */ }
      <MicroAppWithMemoHistory name="demo" url="/demo1" />
      <MicroAppWithMemoHistory name="demo" url="/demo2" />
      <MicroAppWithMemoHistory name="demo" url="/demo3" />
      <MicroAppWithMemoHistory name="demo" url="/demo4" />
      <MicroAppWithMemoHistory name="demo" url="/demo5" />
      <MicroAppWithMemoHistory name="demo" url="/demo6" />
    </div>
  )
}

更多的想象空间

工程上的想象空间

微前端架构除了其带来的巨石应用解构、技术栈无关等工程能力外,也为我们对一些已有的工程问题带来了新的解题思路,比如:

npm 包分发业务组件背后的工程问题

在以前,我们经常通过发布 npm 包的方式复用/共享我们的业务组件,但这种方式存在几个明显的问题:

  1. npm 包的更新下发需要依赖产品重新部署才会生效
  2. 时间一长就容易出现依赖产品版本割裂导致的体验不一致
  3. 无法灰度
  4. 技术栈耦合

说白了就是 npm 包这种静态的共享方式,丧失了动态下发代码的能力,导致了其过慢的工程响应速度,这在现在云服务流行的时代就会显得格外扎眼。而微前端这种纯动态的服务依赖方式,恰好能方便的解决上面的问题:被依赖的微应用更新后的产物,在产品刷新后即可动态的获取到,如果我们在微应用加载器中再辅以灰度逻辑,那么动态更新带来的变更风险也能得到有效的控制。

新的 UI 共享模式

在以前,如果我们希望复用一个站点的局部 UI,几乎只有这样一条路径:从业务系统中抽取出一个组件 -> 发布 npm 包 -> 调用方使用 npm 包。

且不说前面提到的 npm 自身的问题,单单是从一个已有的业务系统中抽取出一个 UI 组件这个事情,都可能够我们喝一壶的了。我们不仅要在物理层面,将这部分代码抽取成若干个单独的文件,同时还要考虑如何跟已有的系统做上下文解耦,这类工作出现在一个越是年代久远的项目上,实施起来就越是困难,做过这类事情的同学应该都会深有体会。

不同于组件库的研发流程,微前端的场景下,大部分时候我们不会为了去复用一个 UI,而去专门写一个微应用出来。通常我们期望的是,从一个已有系统中,直接选取我们需要复用的部分,嵌入到我们自己的容器里进行渲染。

基于上面提到过的微应用多实例的渲染方案,我们可以考虑将需要复用的组件,以路由 URL 作为 ID 的方式导出。比如我们有这样一个 A 应用有一个这样的页面:

function OnePage() {
  return (
    <div>
      <SearchForm/>
      <UserList/>
    </div>
  )
}

我们有另外一个应用,希望单独复用 A 应用的用户列表部分的交互跟 UI,那我们只需要多加一条路由规则:

<Switch>
  ...
  <Route path="/userList" memory={true}>
    <UserList/>
  </Route>
</Switch>

依赖方只需要配合 memory history 并指定 url 为 /userList 即可完成渲染(参考上面嵌套渲染章节)。

站点即配置

当我们将所有可以共享的服务单元变成一个个独立的微应用之后,我们便可以通过配置的方式描述我们的站点了,类似:

{
  layout: 'admin-pro',
  apps: [
    { name: 'abc', props: {} },
    { name: 'bcd', props: {} },
    { name: 'cde', props: {} },
  ]
}

其中 layout 可以是一个带基础交互框架、用户鉴权等公共能力的微应用,也可以是一组微应用的集合(类似 babel 中的 preset 插件),而 apps 则是一组需要在当前站点渲染出来的业务微应用。

这种方式非常适用于,当我们要在一个业务域下开发多个细分服务站点时,通过这种配置的方式,配合一个运行时的微应用编排引擎,快速的生成一系列视觉一致、交互统一的业务站点出来。

产品上的想象空间

在有了上面那些场景背后的技术支撑后,在产品上,我们就已经多出很多想象空间了。

但我们还可以想的更极限一点:

比如我们知道微信有一个公众号浮窗的功能,包括安卓系统常见的小窗模式,解决的就是空间独占、以及跨空间时的交互问题。

image.png

那我们在中后台也可以参考类似的设计,将不同空间的关联性操作以这种非独占的形态聚合到一起,从而降低流程上的断层感,提升产品体验。

demo.jpg

(这里本有一张蚂蚁内部系统的交互示意图,因涉及保密信息无法公开,有兴趣的同学可以选择加入我们后做进一步的交流分享😆)

写在最后

微前端提供的这些渐进式更新、动态组合、 服务拓展的能力,相信大家通过我们介绍的这些常见场景,以及一些极致条件下的解决方案中能窥其一二。

但需要强调的是,任何技术架构都不可能是银弹,我们不必对微前端过于神化/劣化。本篇仅希望在分享了我们在微前端领域的一系列探索之后,能给其他开发者带来一些新的选择和启发,从而为其工程及产品上带来更多的可能性。

最后的最后,诚招天下英雄

上面提到的这些案例,包括一些想象中的场景,正在我们团队内有条不紊地发生着,只是时机未到还不能更详细的对外宣传并与大家分享,欢迎有兴趣的同学可以加入我们,与我们一起去探索更多微前端的可能性。

如果您对微前端感兴趣,请发简历到 youzhi.lk@antfin.com,我们非常期望有机会能与您共事,探索出微前端下更多的场景可能性。

如果您对微前端没什么兴趣,不要紧,只需要您对 antd、AntV、dva、umi、eggjs、ahooks 中任一类别的领域感兴趣,您也可以发简历到 youzhi.lk@antfin.com 来与我们共商大事。

团队介绍

  • 大部门:蚂蚁集团体验技术部
  • 部门主管:玉伯
  • 部门开源作品:antd、AntV、dva、umi、eggjs、qiankun、ahooks 等
  • 部门商业化产品:语雀
  • 直属部门:平台前端技术部 - 部门主管:偏右(antd 负责人),部门介绍:https://www.yuque.com/afx/about/skyline
  • 要求:基本没要求,p5~p8 我们都需要,只要您想来试试就可以投简历到 youzhi.lk@antfin.com,HC 非常多!!
  • base 地:杭州、上海、成都 三地任选
@yupeng-yuxiaoyu
Copy link

umijs/qiankun#1423
最近在使用qiankun的时候,也遇到了render两次的问题,在这个issues中提到目前无解,我看single-spa在v4.3.1已经说解决了这个问题,为啥还不行嘞?

也有看到{ urlRerouteOnly: true }这种方式,但是也是不生效的。

麻烦问下,现在有什么办法进行规避嘛?

@Sally-he
Copy link

@yupeng-yuxiaoyu 同遇见,不知道你解决没

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants