写点什么

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

  • 2023-06-05
    上海
  • 本文字数:4137 字

    阅读完需:约 14 分钟

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.tsconst CommonPanes = {   ReplayPane,   ReplayCasePane,};const ExtraPanes = {}; // 对外暴露的扩展配置const Panes = Object.assign(CommonPanes, ExtraPanes);export default Panes
// Layout.tsxconst 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

用户头像

https://github.com/arextest 2023-01-11 加入

AREX 是一个基于真实请求与数据的自动化回归测试平台。通过复制线上真实流量到测试环境进行自动化回归测试,解决回归测试的难题。

评论

发布
暂无评论
自动化回归测试平台 AREX 前端架构演变史 —— Tabs 动态组件设计_Vue_AREX 中文社区_InfoQ写作社区