技术拆解-文本可视化T8

T8是蚂蚁集团AntV技术栈下的一款文本可视化解决方案,采用声明式JSON Schema语法来描述数据解读报告的内容。作为一个专门为AI时代设计的可视化库,它在架构设计上有诸多值得深入研究的亮点。

开源源码阅读方法

与项目汇报相似,阅读开源源码应采用自顶向下、逐步求精的方法:

  1. 先看宏观调用流程 - 理解整体架构和入口点
  2. 再深入具体实现 - 分析各个模块的细节

架构设计思考

整体Manager封装的缺失

我们项目中缺少统一的Manager封装,可以参考:

  • PluginManager模式 - 统一管理所有插件的注册和调用
  • ECharts的extension.ts - 在该文件中封装所有的Register操作
  • T8的简洁应用 - 核心流程只需要3-4个函数调用

Facade模式的应用

通过Facade模式统一对外接口,隐藏内部复杂性,让外部调用更加简洁。

文本可视化的服务化封装

将文本可视化封装为服务+组件的组合:

  • 服务层:负责数据处理、Schema解析、插件管理
  • 组件层:负责UI渲染和用户交互
  • 目标:使其能够在各处复用

VISALL的UI组件编排设计

参考T8的Preact实现:

  • 虚拟DOM组件 - 提升渲染性能
  • 组件编排机制 - 支持灵活的界面组合
  • HIVIS也可以采用类似设计 - 统一的组件架构模式

Schema设计:逻辑抽离与AI友好

Schema+Spec架构模式

1
Schema(逻辑层) + Spec(表现层) = AI友好的架构

这种模式的核心优势:

  • 逻辑抽离到Schema - 组件只负责渲染
  • AI友好设计 - 便于大模型理解和生成
  • 多模态支持 - 图表+文本+表格的统一描述

本质上这和KAmis类似,都是通过DSL/Schema作为中间层。AI短期内无法直接生成可用的产品界面,但可以:

  1. 先生成DSL/Schema - 结构化描述界面需求
  2. 再通过解释器渲染 - 将Schema转换为可视化界面

Entity设计

包括验证方式(如zod)和UI表现的定义:

  • 数据验证 - 确保输入数据的正确性
  • UI映射 - 将数据转换为可视化元素
  • 交互处理 - 定义用户交互行为

组件库作为解释器引擎

与传统架构的对比:

  • ECharts模式:绘制逻辑放在组件内部
  • T8模式:绘制逻辑放在Schema中

这种模式变更带来的影响:

  • 开发主体变化:从业务开发人员维护 → AI维护
  • 个性化能力:能实现千人千面的定制化
  • 对比HIVIS:需要思考如何适配这种新模式

CustomBlock机制

自定义块机制提供了强大的扩展能力:

  • 自定义渲染逻辑 - 支持特定的业务需求
  • 插件化架构 - 便于功能模块的独立开发
  • 动态加载 - 按需加载自定义组件

Plugin机制:业务个性化实现

插件系统是实现业务个性化的关键:

  • 标准化接口 - 统一的插件开发规范
  • 动态扩展 - 运行时加载和卸载插件
  • 隔离机制 - 插件间的解耦和独立

架构设计原则

通用机制提取

将通用的机制、类、框架提取到单独的仓库和包中:

  • EventEmitter - 事件处理机制
  • 通用工具类 - 可复用的基础组件
  • 框架层 - 标准化的开发框架

这种模块化设计能够:

  • 提高代码复用率
  • 降低维护成本
  • 便于团队协作

核心架构:JSON Schema驱动的数据流

T8的核心设计理念是”数据即视图”,整个项目以JSON Schema为数据流主体,通过插件化架构实现功能扩展。

外层Text组件设计

1
2
3
4
5
export class Text extends EE {
private spec: NarrativeTextSpec; // JSON Schema作为核心数据结构
private pluginManager: PluginManager; // 插件管理器
private parser: T8ClarinetParser; // 流式解析器
}

Text类作为整个渲染引擎的入口,承担了三个核心职责:

  • 数据管理:通过schema()方法接收NarrativeTextSpec数据
  • 插件协调:通过PluginManager管理所有可插拔模块
  • 渲染控制:支持一次性渲染和流式渲染两种模式

流式JSON支持:面向AI时代的设计

T8最前瞻的设计是对流式JSON输出的支持。通过封装clarinet库实现流式JSON解析器:

1
2
3
4
5
6
7
8
9
10
11
export const streamRender = (newJSONFragment: string) => {
this.parser.append(newJSONFragment);
const result = this.parser.getResult();
if (result.error) {
options?.onError?.(result.error);
} else {
options?.onComplete?.(result);
this.schema(result.document as NarrativeTextSpec);
this.render();
}
};

这种设计完美契合大模型逐步输出的工作模式,为实时数据可视化提供了基础支撑。

技术选型:Preact代替React

在包体积限制(压缩后75KB,gzip后25KB)的约束下,T8选择了Preact作为UI框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { h, render as preactRender } from 'preact';

render() {
preactRender(
h(NarrativeTextVis, {
spec,
pluginManager: this.pluginManager,
themeSeedToken: this.themeSeedToken,
onEvent: this.emit.bind(this),
}),
container,
);
}

Preact保持了React的hooks、render等API,但显著降低了库的体积,这对于组件库开发是理想选择。

插件系统:描述符驱动的扩展架构

T8的插件系统采用了描述符(Descriptor)模式,通过统一的标记系统来管理和识别不同类型的可插拔模块。

描述符系统的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export interface PhraseDescriptor<MetaData> {
key: string;
isEntity: boolean;
render?: ((value: string, metadata: MetaData) => HTMLElement) | HTMLElement;
tooltip?: ...;
classNames?: ...;
style?: ...;
}

export interface BlockDescriptor<CustomBlockSpec> {
key: string;
isBlock: true;
className?: ...;
style?: ...;
render?: (spec: CustomBlockSpec) => HTMLElement;
}

“描述符”这个命名比传统的”type”、”name”更加贴切,准确表达了其在系统中作为识别标记的作用。

PluginManager的统一管理

1
2
3
4
5
6
7
8
9
10
11
export class PluginManager {
protected entities: Partial<Record<EntityType, PhraseDescriptor<EntityMetaData>>> = {};
protected customPhrases: Record<string, PhraseDescriptor<any>> = {};
protected customBlocks: Record<string, BlockDescriptor<any>> = {};

register(plugin: PluginType) {
if (isBlockDescriptor(plugin)) this.customBlocks[plugin.key] = plugin;
if (isEntityDescriptor(plugin)) this.entities[plugin.key] = plugin;
if (isCustomPhraseDescriptor(plugin)) this.customPhrases[plugin.key] = plugin;
}
}

PluginManager统一管理三种类型的插件:实体短语(Entity)、自定义短语(Custom Phrase)、自定义块(Custom Block),实现了真正的功能解耦。

组件架构:榫卯结构与洋葱模型

T8的内部组件架构体现了经典的”榫卯结构”设计,通过插槽和组件化的方式构建复杂的可视化界面。这种设计也可以看作是一种洋葱模型,逐层封装不同的职责。

ContextProvider的洋葱模型

1
2
3
4
5
6
7
8
9
10
11
export const ContextProvider = ({ plugin, themeSeedToken, events, children }) => {
return (
<PluginProvider plugin={plugin}> {/* 最外层:插件层 */}
<ThemeProvider themeSeedToken={themeSeedToken}> {/* 中间层:主题层 */}
<EventProvider events={events}> {/* 内层:事件层 */}
{children}
</EventProvider>
</ThemeProvider>
</PluginProvider>
);
};

这种三层嵌套结构清晰地分离了插件管理、主题配置和事件处理三个关注点。

NarrativeTextVis的容器结构

1
2
3
4
5
6
7
8
9
10
const { headline, sections, styles, className } = spec;

return (
<ContextProvider themeSeedToken={themeSeedToken} plugin={pluginManager} events={events}>
<Container>
{headline ? <Headline spec={headline} /> : null}
{sections?.map((section) => <Section key={section.key || v4()} spec={section} />)}
</Container>
</ContextProvider>
);

层级关系

整个组件架构的层级关系非常清晰:

1
2
3
4
5
6
7
8
ContextProvider (全局上下文)
└── Container (主容器)
├── Headline (标题 - 朴素短语序列)
└── Sections (内容模块)
└── Paragraphs (段落)
├── Phrases (短语序列 - normal类型)
├── Bullets (条目列表)
└── CustomBlock (自定义块)

数据结构:从NarrativeTextSpec到原子组件

JSON Schema作为数据流主体

T8的JSON Schema定义了完整的数据结构层次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"NarrativeTextSpec": {
"properties": {
"headline": {"$ref": "#/definitions/HeadlineSpec"},
"sections": {
"items": {"$ref": "#/definitions/SectionSpec"},
"type": "array"
}
}
},
"SectionSpec": {
"anyOf": [
{"properties": {"paragraphs": {...}}},
{"properties": {"customType": {...}}}
]
}
}

数据流向清晰:NarrativeTextSpec → headline + sections → SectionSpec → paragraphs → PhraseSpec

标准建制:两种基本段落类型

T8定义了两种标准的段落”建制”:

  1. Phrases(短语序列):normal类型的文本段落
  2. Bullets(条目列表):有序或无序列表
1
2
3
4
5
6
7
8
9
10
11
export function Bullets({ spec }: BulletsProps) {
const children = spec.bullets?.map((bullet) => (
<Li key={spec.key || v4()}>
<Phrases spec={bullet.phrases} />
{bullet?.subBullet ? <Bullets spec={bullet?.subBullet} /> : null}
</Li>
));

const tag = spec.isOrder ? 'ol' : 'ul';
return <Comp as={tag}>{children}</Comp>;
}

原子组件:Phrase、Entity与Chart

Phrase:朴素文本短语

Phrase是最基础的文本元素,支持丰富的样式定制:

1
2
3
4
5
6
if (isTextPhrase(phrase)) {
if (phrase.bold) defaultText = <Bold>{defaultText}</Bold>;
if (phrase.italic) defaultText = <Italic>{defaultText}</Italic>;
if (phrase.underline) defaultText = <Underline>{defaultText}</Underline>;
if (phrase.url) defaultText = <a href={phrase.url}>{defaultText}</a>;
}

Entity:关键信息实体

Entity是展示关键信息的核心元素,预定义了10种类型:

1
2
3
4
5
6
7
"EntityType": {
"enum": [
"metric_name", "metric_value", "other_metric_value",
"contribute_ratio", "delta_value", "ratio_value",
"trend_desc", "dim_value", "time_desc", "proportion"
]
}

每个实体都包含丰富的元数据:

1
2
3
4
5
6
7
8
9
"EntityMetaData": {
"properties": {
"entityType": {"description": "实体类型标记"},
"origin": {"description": "原始数据", "type": "number"},
"assessment": {"description": "衍生指标评估参数"},
"detail": {"description": "明细数据,用于弹框展示"},
"sourceId": {"description": "变量来源ID"}
}
}

Chart:轻量级数据可视化

Chart在T8中不是独立的可视化组件,而是作为实体的辅助表现形式,提供”一看就懂”的数据展现。

以Proportion图表为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const renderProportionChart = (container: Element, config: ProportionChartConfig) => {
const { data = 0 } = config;
const chartSize = getElementFontSize(container);
const proportion = Math.max(0, Math.min(1, data));

const r = chartSize / 2;
const svg = createSvg(container, chartSize, chartSize);

// 背景圆 + 弧形段
svg.append('circle').attr('cx', r).attr('cy', r).attr('r', r).attr('fill', '#CDDDFD');
const endAngle = proportion * 2 * Math.PI;
const arcPath = arc(r)(r, r, endAngle);
svg.append('path').attr('d', arcPath).attr('fill', '#3471F9');
};

通过SVG路径直接渲染,舍弃了复杂的视图对象和交互动画,专注于信息传递的本质。

事件处理:完整的冒泡机制

T8实现了完整的事件冒泡机制,每个层级都支持click、mouseenter、mouseleave事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Phrase层级
const handleClick = () => {
onClick?.(spec?.value, metadata);
onEvent?.('phrase:click', spec);
};

// Headline层级
const onClick = () => {
onEvent?.('paragraph:click', spec);
};

// NarrativeTextVis层级
const onClick = () => {
onEvent?.('narrative:click', spec);
};

这种设计确保了用户交互的精确捕捉和处理。

设计思想:大模型友好的架构

“数据即视图”的理念

T8的JSON Schema设计(442行完整定义)让人联想到SVG的”语言描述系统”。通过声明式的配置而非命令式的编程,降低了维护门槛,特别适合AI时代的自动化需求。

为AI流式输出优化

流式JSON解析器的设计完全考虑了大模型逐步输出的特点:

  • 支持增量解析:通过append()方法接收JSON片段
  • 错误容错:提供完整的错误处理机制
  • 渐进渲染:解析完成后立即渲染,无需等待完整数据

类型安全与自动约束

通过TypeScript类型定义自动生成JSON Schema,传递给LLM作为输出约束:

1
ts-json-schema-generator -f tsconfig.json -p src/index.ts -t NarrativeTextSpec

这种设计确保了大模型输出的格式正确性,实现了”数据驱动的升级版”。

架构优势与启发

设计亮点

  1. 高度模块化:插件系统实现了真正的功能解耦
  2. 数据驱动:JSON Schema作为单一数据源,便于管理和传输
  3. 扩展性强:通过描述符模式支持任意类型的内容扩展
  4. 性能优化:Preact + 流式渲染,适合大数据量场景
  5. AI友好:流式支持和声明式配置迎合了AI时代的开发需求

对其他项目的启发

对于类似的可视化项目,T8的设计提供了很好的参考:

  1. 统一的容器-插槽模式:ContextProvider + PluginManager + Component
  2. 标准化的数据接口:JSON Schema作为通用语言
  3. 灵活的扩展机制:通过descriptor实现插件化
  4. 清晰的层级结构:从全局上下文到原子组件的逐层细化

这种设计既保证了整体的统一性,又为差异化需求留出了足够的扩展空间。

资料