开源测试工具 自动化回归测试平台 AREX 前端架构演变史 —— Tabs 动态组件设计

lijing-22 · 2023年06月05日 · 2305 次阅读

AREX(http://arextest.com/)是一款开源的基于真实请求与数据的自动化回归测试平台,利用 Java Agent 技术与比对技术,通过流量录制回放能力实现快速有效的回归测试。同时提供了接口测试、接口比对测试等丰富的自动化测试功能。

在这个系列中,我们将会介绍 AREX 前端架构的演变过程,以及在演变过程中遇到的问题和解决方案,一来作为开发过程的经验分享,二来方便大家对 AREX 源码的理解以及二次开发。

Tabs 动态组件设计

Tabs organize content across different screens, data sets, and other interactions —— Material Design

Tab 组件在前端开发中有着广泛的应用场景,它能够满足各种不同的需求,如数据展示、导航菜单、表单填写、产品分类、菜单展示、图片幻灯片等多种场景。在 AREX 中也有大量使用 Tabs 组件的场景,其中最具代表性的是 AREX 主工作区。在这篇文章中,我将会介绍 AREX 主工作区中 Tabs 动态组件的设计思路和实现迭代过程。

主工作区是 AREX 的核心功能区域,用户在这里完成 API 调试、录制用例回放、环境管理、应用配置等一系列任务。由于提供的功能众多,用户使用流程非线性,经常需要在多个功能模块之间切换,为了提供良好的用户体验和高效的操作方式,AREX 主工作区采用了 Tab 组件来组织和展示主工作区的不同功能页面模块。

用户在主工作区借助 Tabs 快速地切换到所需的功能页面,同时 Tabs 在切换时对功能页面进行缓存,避免了频繁地重新初始化功能页面的操作,提高了用户的体验和使用效率。这看起来是一个理所应当且很简单的功能,毕竟 Tabs 本身的功能已在 UI 框架中实现,但是在实际的过程中还是碰到了一些问题。

1.0 —— 原始的条件渲染

在 AREX 的早期版本中,Tabs 组件采用的是 Ant Design 4.23.0 之前的 JSX 拼接写法。Tabs 组件实现的简化代码如下:

 <Tabs
  activeKey={activePane}
  onEdit={handleTabsEdit}
  onChange={setActivePane}
> 
  {panes.map((pane) => (
    <Tabs.TabPane
      key={pane.key}
      tab={genTabTitle(pane)}
    >
      {pane.paneType === PaneTypeEnum.Replay && (
        <Replay data={pane.data as ApplicationDataType} />
      )}
      {pane.paneType === PaneTypeEnum.ReplayCase && (
        <ReplayCase data={pane.data as PlanItemStatistics} />
      )}
    </Tabs.TabPane>
  ))}
</Tabs>

主工作区中的 panes 负责维护各个功能页面的种类、名称、页面初始化参数等数据,并由全局状态进行管理。当新增或关闭功能页面时,触发 panes 的更新操作。

主工作区的 Tabs.TabPane 组件通过条件渲染来实现。在对 panes 进行遍历时,根据每个 pane 的 paneType 属性匹配到相应的页面组件,并进行渲染。这种实现方式的优点在于简单直接,通过条件判断能够实现页面的动态渲染。然而,这种方式也存在一些缺点,例如大量的条件判断语句降低了代码的可读性和优雅性。同时,可扩展性也较差,当需要新增一个功能页面时,需要修改 Tabs 组件的代码,违反了开闭原则。

因此,为了解决这些问题,我们自然而然地想到需要一种更加通用的实现方式,以适配未来可能出现的更多未知的功能页面。

2.0 —— 动态组件渲染

由于 Tabs 组件中 TabPane 所对应的实际组件是不确定的,它随着用户的操作而动态变化,因此我们想到将 TabPane 由原先的条件渲染组件改为动态组件。通过动态组件的方式,我们可以根据配置信息动态地加载和渲染功能页面,实现了更高的可扩展性和灵活性。这样一来,新增功能页面时只需配置相应的参数,而无需修改 Tabs 组件的代码,大大减少了维护和扩展的成本。这种改进不仅提升了代码的可读性和优雅性,也为未来的功能扩展奠定了坚实的基础。

在前端开发中,动态组件是一种广泛应用的技术,它可以帮助我们实现高度灵活和可复用的组件,并满足动态渲染组件的需求。动态组件的概念是基于组件化开发的理念,将页面拆分为独立的可重用单元,通过动态加载和渲染这些组件,实现灵活的页面构建和交互。

在 Vue 框架中,我们可以通过 <component v-bind:is="componentName"></component>的方式来实现动态组件。其中,componentName可以是一个变量,根据需要动态指定不同的组件名称,从而实现组件的动态加载和渲染。

而在 React 框架中,我们可以通过声明一个组件的变量,然后将这个变量作为 JSX 的标签名来实现动态组件。这种方式允许我们根据需要在运行时选择不同的组件进行渲染。此外,我们还可以使用 React.createElement() 方法来动态创建并渲染组件。

在 AREX 升级适配 Ant Design 5.0 的重构版本中,我们使用了第二种方式来实现动态组件,同时使用了 Ant Design 提供的新的 Tabs 简写方式,用 item 有更好的性能和更方便的数据组织方式。优化后的动态 Tabs 组件的简化代码如下:

// Panes.ts
const CommonPanes = {
   ReplayPane,
   ReplayCasePane,
};
const ExtraPanes = {}; // 对外暴露的扩展配置
const Panes = Object.assign(CommonPanes, ExtraPanes);
export default Panes

// Layout.tsx
const tabsItems = useMemo(
  () =>
       panes.map((pane) => {
       const Pane = Panes[pane.paneType];
       return {
         key: pane.key,
         label: genTabTitle(pane),
         children: <ErrorBoundary>{React.createElement(Pane, { data: pane })}</ErrorBoundary>,
      };
    }),
  [panes],
);

<Tabs
   items={tabsItems}
   activeKey={activePane}
   onEdit={handleTabsEdit}
   onChange={setActivePane}
/>

在这个方案中,我们将功能页面的种类和组件映射逻辑从 Tabs 组件中抽离出来,放到了一个单独的文件中,这样做的好处是将 Tabs 组件的职责进行了拆分,使得 Tabs 组件内部不再关注具体被注册的功能页面组件,同时也方便了功能页面的扩展。

除此之外还有两个细节应当注意,其一是在 Panes.ts 中定义了 ExtraPanes 对象,用于对外暴露扩展组件的配置,供对该项目的二次开发者使用,提供专用的配置空间可以实现二次开发代码与源代码的隔离,防止二次开发在与源代码同步时发生代码冲突,这样的配置空间隔离在 AREX 的设计中也有多处体现;其二是在动态组件外使用了 ErrorBoundary 组件对其包裹,用于捕获因非法的 paneType 造成的动态组件渲染失败和动态组件内部的错误,避免导致整个页面的崩溃。

3.0 —— 组件注册管理

在上个方案中我们已经提供了一个专用的配置对象,以解决动态组件的拓展性问题。但是局限性仍然存在,我们希望能够提供一个更加通用、灵活的组件注册管理方案,以摆脱只能在 Panes.ts 文件的 ExtraPanes 中进行配置功能组件的限制。

这个问题在 AREX monorepo 重构过程中显得尤为重要,原因是在 monorepo 版本中,AREX 被拆分成 arex-core 公用纯函数组件包和 arex 业务逻辑包,负责渲染主工作区的 Tabs 组件被封装到 arex-core 包中,而 tabsItems 在遍历时需要用到的映射配置 Panes 在两个包中均有定义,集中化的配置方式已经不能满足需求,我们需要一种可以分布式、多次进行的组件注册管理方案。

为了解决这个问题,我们设计了 ArexPanesManager 容器,用于管理功能页面组件的注册和获取,其简化代码如下:

export class ArexPaneManager {
  private static panesMap: Map<string, ArexPane> = (() => {
    const map = new Map<string, ArexPane>();
    for (const pane in ArexPanes) {
      map.set(pane, ArexPanes[pane]);
    }
    return map;
  })();

  public static getPanesMap(): Map<string, ArexPane> {
    return this.panesMap;
  }

  public static registerPanes(panesMap: {[key: string]: ArexPane }) {
    for (const name in panesMap) {
      const pane = panesMap[name];
      if (this.panesMap.has(pane.type)) continue;
      this.panesMap.set(pane.type, pane);
    }
  }

  public static getPaneByType<T extends PanesData>(type?: string): ArexPane<T> | undefined {
    return (
      this.panesMap.get(type || ArexPanesType.PANE_NOT_FOUND) ||
      ArexPanes[ArexPanesType.PANE_NOT_FOUND]
    );
  }
}

借助该容器提供的 ArexPaneManager.registerPanes() 方法,可以实现在不同包中分别注册功能页面组件,最终在 arex-core 包中的 Tabs 组件借助 ArexPaneManager.getPaneByType() 方法统一获取完整的功能页面组件的映射配置。

值得一提的是,为了统一所有功能页面组件 Pane 的规范,我们设计了标准的 ArexPane 类型,所有出现在主工作区 Tabs 下的功能页面组件都应当继承该类型。为此我们提供了 ArexPaneFC 类型和 createArexPane() 方法,所有的页面功能组件先由 ArexPaneFC 定义,再经 createArexPane() 方法封装后,便得到可注册至 ArexPaneManager 进行维护管理的 ArexPane 功能页面组件。相关的类型定义和方法简化代码如下:

export type Pane<D extends PanesData = PanesData> = {
    id: string; 
    key?: string; 
    type: string;
    data?: D;
};

export type ArexPaneOptions = {
    icon?: React.ReactNode;
    type: string;
};

export type PanesData = any;

export type ArexPane<D extends PanesData = PanesData> = ArexPaneFC<D> & ArexPaneOptions;

export type ArexPaneFC<D extends PanesData = PanesData> = React.FC<{ data: D }>;

export function createArexPane<D extends PanesData>(
  Pane: ArexPaneFC<D>,
  options: ArexPaneOptions,
): ArexPane<D> {
  const { noPadding = false } = options;
  return Object.assign(Pane, { ...options, noPadding });
}

经过三个版本的演变,我们最终实现了一个通用的、分布式的功能页面组件注册管理方案,该方案在 AREX monorepo 重构中得到了应用,也为 AREX 的二次开发提供了更多的可能性和便利性。代码的封装性和扩展性一直是开发过程中需要考虑的重要因素,我们在 AREX 的开发过程中也一直在思考如何提高代码的封装性和扩展性,希望能够为二次开发者提供更好的开发体验。

  • AREX 文档:arextest.com/zh-Hans/doc…

  • AREX 官网:arextest.com/

  • AREX GitHub:github.com/arextest

  • AREX 官方 QQ 交流群:656108079

暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册