news 2026/9/15 17:15:45

@astryxdesign/vega:在 React 中渲染 Vega / Vega-Lite 规范的 Astryx 图表封装组件指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@astryxdesign/vega:在 React 中渲染 Vega / Vega-Lite 规范的 Astryx 图表封装组件指南

@astryxdesign/vega:在 React 中渲染 Vega / Vega-Lite 规范的 Astryx 图表封装组件指南

【免费下载链接】astryxAn open source design system that's fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx

@astryxdesign/vega是 Astryx 设计系统提供的 Vega 图表封装包,它以单个 React 组件<VegaChart>为入口,将 Vega 与 Vega-Lite 的完整能力(编译、解析、View 生命周期)透传给 React 应用。本文从安装、用法、完整 API 到 View 生命周期按值比较的重建策略、数据加载契约与不受信任 spec 的安全边界,结合包内源码(VegaChart.tsx、viewInputs.ts、schema.ts 等)与测试用例逐层展开。读完本文,你将能够:正确安装并组合 Vega/Vega-Lite 渲染管线、精通parseConfig/parseOptions/viewOptions/compileOptions四个透传配置的取值与作用、理解组件"按值而非按引用"重建 View 的设计及其边界,并为不可信 spec 搭建解释器 + 受限 loader 的安全渲染方案。

包定位:一个组件,两条渲染管线

@astryxdesign/vega在 packages/vega 目录下实现,官方定位是 "chart and data visualization components"(图表与数据可视化组件)。它本身不重复造轮子,而是通过 Vega 运行时渲染 Vega 与 Vega-Lite 规范:

  • Vega-Lite 规范:组件检查$schema后调用vega-litecompile()先编译为 Vega 规范,再渲染;
  • Vega 规范:跳过编译,直接交给vega.parse()渲染;
  • 无效或缺失$schema:调用onError且不渲染任何内容。

组件在 VegaChart.tsx 中完整呈现了这条管线:先由parseSchema(spec.$schema)校验并判定库类型,Vega-Lite 规范经compile(spec, compileOptions).spec编译,随后parse(vegaSpec, parseConfig, parseOptions)得到 Runtime,最后new View(runtime, {hover: true, ...viewOptions, container})构造视图——container始终由组件注入,覆盖调用方传入的同名值。测试 VegaChart.test.tsx 亦验证了这一分支:Vega-Lite spec 会且只会调用一次compile,而原生 Vega spec 不会触碰 vega-lite 编译器。

与 Astryx 其他组件不同,本包不依赖 StyleX,因此<VegaChart>不接受xstyleprop,布局覆盖请使用classNamestyle(源码注释见 VegaChart.tsx)。同时组件扩展了React.HTMLAttributes<HTMLDivElement>(剔除contentEditabledangerouslySetInnerHTML等),可将任意 DOM 事件、data-testidaria-label等透传到容器 div 上。

安装:只有@canary,没有latest

Vega 包的发布状态比较特殊:它只以@canarydist-tag 发布到 npm,尚无 stable(latest)版本,因此安装时必须显式指定 tag:

npm install @astryxdesign/vega@canary vega vega-lite

两个要点需要注意:

  1. Peer 依赖需自行安装vegavega-lite(以及react >=19.2.0react-dom >=19.2.0)是 package.json 中声明的 peerDependencies,vega >=6.0.0vega-lite >=6.0.0。VegaChart 组件内部大量使用 React 19 的useEffectEvent(见 VegaChart.tsx),这是它要求 React 19.2+ 的直接原因。
  2. Canary 构建跟踪main分支最新提交,版本形如0.x.y-canary.<sha>,任意两个 canary 之间都可能发生破坏性变更——需要稳定性就锁死精确版本。

canary-only 的机制由 package.json 中的"astryx": {"canaryOnly": true}"private": true双重标记保证(详见下文"构建与发布"一节)。

快速上手:两种 spec 的写法

Vega-Lite 规范(自动编译)

传一个带$schema的 Vega-Lite 顶层规范对象即可,组件自动完成编译与渲染:

import {VegaChart} from '@astryxdesign/vega'; <VegaChart spec={{ $schema: 'https://vega.github.io/schema/vega-lite/v5.json', mark: 'bar', data: { values: [ {a: 'A', b: 28}, {a: 'B', b: 55}, ], }, encoding: { x: {field: 'a', type: 'ordinal'}, y: {field: 'b', type: 'quantitative'}, }, }} />;

Vega 规范(直接渲染,不编译)

传原生 Vega 规范时,vega-lite编译器不会被调用,parse()直接消费原始 spec:

<VegaChart spec={{ $schema: 'https://vega.github.io/schema/vega/v5.json', marks: [...], }} />

完整配置:从 compile 到 View 的全链路透传

<VegaChart>的设计哲学是把 Vega 的底层 API 原样暴露为 props:parseConfigparseOptions对应vega.parse(spec, config, options)viewOptions对应new vega.View(runtime, options)compileOptions对应 Vega-Lite 的compile(spec, options)。下面的例子展示了全部透传点:

<VegaChart spec={spec} parseConfig={{background: '#1a1a1a'}} parseOptions={{ast: true}} viewOptions={{ renderer: 'canvas', logLevel: 1, tooltip: myTooltipHandler, locale: myLocale, loader: myLoader, }} onReady={view => { view.addSignalListener('highlight', (name, value) => { console.log('signal:', name, value); }); }} onError={err => console.error('Chart error:', err.message)} />

Prop 总表

Prop类型默认值说明
specAnySpec--$schema的 Vega 或 Vega-Lite 规范(必填)
dataViewData--初始数据集:{datasetName: tuples[]}
compileOptionsCompileOptions--传给compile(spec, options),仅 Vega-Lite 生效
parseConfigConfig--传给parse(spec, config)的 Vega 配置
parseOptionsParseOptions--传给parse(spec, config, options)的选项
viewOptionsOmit<ViewOptions, 'container'>--传给new View(runtime, options)的选项
classNamestring--容器 div 的 CSS 类
styleCSSProperties--容器 div 的内联样式
onReady(view: View) => void--View 就绪后回调,携带活的 Vega View
onError(err: Error) => void--schema 错误、编译失败或渲染失败时回调

注意AnySpec = (VegaSpec | VegaLiteSpec) & {$schema: string}(见 types.ts)——$schema是类型层面的硬性要求,这与运行时校验一致。

viewOptions:View 构造选项

直接映射到 Vega 的ViewOptionscontainer被剔除(始终由组件注入)。常用字段:

字段类型说明
renderer'svg' \| 'canvas'渲染后端(默认'svg'
hoverboolean启用 hover 编码(默认true
logLevelnumberVega 日志详细程度
loggerLoggerInterface自定义 logger
tooltipTooltipHandler自定义 tooltip 处理器
localeLocaleFormatters数字与时间格式化 locale
loaderLoader自定义数据 loader
backgroundColor图表背景色

源码中hover: true是组件给出的默认值,随后被viewOptions展开覆盖(VegaChart.tsx);测试 VegaChart.test.tsx 验证了viewOptions={{hover: false}}能正确覆盖默认值,同时container仍由组件注入。

compileOptions:仅 Vega-Lite 生效

字段类型说明
configVegaLiteConfig在 spec 自带 config 之上合并的 Vega-Lite 配置
loggerLoggerInterface编译期间使用的自定义 logger
fieldTitle(fieldDef, config) => string自定义字段标题格式化器

这套类型并未直接借用 vega-lite 的公共导出(其CompileOptions不在公共 API 面内),而是在 types.ts 中重新声明,并有意将fieldTitle定义为宽松签名以避免耦合 vega-lite 未导出的内部类型。compileOptions对原生 Vega spec 被直接忽略。

parseOptions:AST 保留

字段类型说明
astboolean在运行时保留表达式 AST(默认false,对工具链与解释器模式有用)

ast: true是"不受信任 spec"安全方案的前置条件之一(见下文),因为保留 AST 后表达式不再用Function构造函数编译。

View 生命周期:按"值"而非按"引用"重建

<VegaChart>在挂载时构建 VegaView,卸载时view.finalize()释放资源。两次挂载之间,仅当speccompileOptionsparseConfigparseOptionsviewOptions中某个的"值"发生变化时才重建 View——比较基准是 View 构建时保存的一份值快照,而不是上一次的 props。这带来两个直接推论,且两者都无需useMemo

  • 每次渲染内联重建的对象字面量不会拆掉图表;
  • 在 ref、模块常量或跨渲染共享对象上原地修改spec,会被检测到——因为比较对象是构建时的副本,而非上一次 props。

背后的实现是 viewInputs.ts 中的 latch 机制:latchViewInputs()保存{inputs, snapshot},其中snapshot是运行时依赖 props 的结构副本(viewInputs.ts);latchIsCurrent()用快照逐项比对当前值(viewInputs.ts)。为什么必须与副本比对而不能与"上一次 props"比对?因为原地修改会让新旧 props 指向同一个对象——旧值已丢失,仅比较引用会漏掉变更,导致图表静默过期(源码注释明确点出这一点,viewInputs.ts)。快照比对在渲染期间执行,因此判定必须稳定;VegaChart.test.tsx 覆盖了这些场景:相等值重渲染保持单 View、renderer: 'canvas' → 'svg'触发重建、函数引用变化触发重建、共享嵌套对象被原地修改后重渲染会重建且第二次编译结果反映新字段值。

比较的粒度:结构值 vs 引用值

  • 普通对象与数组逐项比较:内联重建的等价 options 视为未变;
  • 函数(tooltiploggerloaderexprfieldTitle)与类实例按引用比较:两个外表相同的实例行为可能不同,副本无法捕获其方法语义——所以应"替换值"而非"原地修改"。

两种无法复制比较的形状

快照并非无限深拷贝,它设置了MAX_DEPTH = 100深度上限,并用OPAQUE符号标记无法遍历的子树(viewInputs.ts)。有两种形状会退化为按引用比较

  • 引用环(子树重新进入自身路径上的对象):环的回边指向副本已遍历过的对象,因此环不会隐藏任何变更,循环 spec 任意位置的原地修改依然能被检测;
  • 嵌套超过 100 层:深度以下的原地修改不可见(引用没变、值没被复制),需要替换对象或通过onReady驱动 View 更新。

无论哪种情况,凡是被复制到的部分仍按值比较,因此 spec 中其他位置的变更照样被捕获。测试 viewInputs.test.ts 对相同值新字面量、键增删、循环 spec 挂载/重渲染/替换、深度嵌套等边界均有断言。这套"opaque 按引用比较"设计还有一层防呆作用:若无法复制部分每次都比较为"不等",渲染期刷新 latch 将导致 React 无限重渲染("Too many re-renders"),按引用比较则保证同对象稳定、换对象恰好重建一次。

不触发重建的 props

dataclassNamestyleonReadyonError对生命周期完全惰性,永不单独触发 View 重建。onReadyonError通过useEffectEvent实现:回调永远读取最新 props,又不会成为 Effect 的响应式依赖,父组件每次渲染传新内联函数也不会拆掉图表(VegaChart.tsx)。

数据加载:data只做初始化,动态更新请走onReady

data将数据集名映射到元组数组,在 View 初始化阶段、首次渲染之前通过view.data(name, tuples)应用(VegaChart.tsx)。它是非响应式的:挂载后修改被忽略,仅靠新data对象本身永远不会重建 View(当其他因素重建 View 时,新 View 会加载那一刻data持有的值)。

测试 VegaChart.test.tsx 明确断言:仅data变化时 View 不重建、view.data保持只调用一次,且传给 Vega 的是初始旧值;而 spec 变化触发重建时新 View 加载最新 data(VegaChart.test.tsx)。

要在渲染后动态更新数据,用onReady拿住活的 View 自行驱动:

<VegaChart spec={spec} data={{ table: [ {category: 'A', value: 28}, {category: 'B', value: 55}, ], }} onReady={view => { // 之后随时更新数据: view.data('table', newRows); view.runAsync(); }} />

ViewData的类型定义为Record<string, unknown[]>(types.ts),每个 key 必须匹配 specdata数组中定义的数据集名。

安全边界:处理不受信任的 spec

Vega/Vega-Lite spec是程序而非纯数据:Vega 会求值 spec 内的表达式字符串(signals、事件流、encodings、filters),默认用Function构造函数将其编译为 JavaScript;spec 的data项还能命名 URL——包括由 signal 动态构造的 URL——默认 loader 会用页面凭据去抓取。

<VegaChart>以 Vega 默认配置渲染传入的 spec,因此默认值就是信任边界:只传自己编写或审阅过的 spec。若 spec 来自用户输入、持久化文档或模型输出,请通过既有的透传选项接上 Vega 官方的"安全求值"推荐配置:

import {expressionInterpreter} from 'vega-interpreter'; import {loader} from 'vega'; <VegaChart spec={untrustedSpec} // 保留表达式 AST,并以解释方式求值(不用 Function 构造函数)。 // 更慢,且有一小部分表达式不受支持——参见 vega-interpreter 文档。 parseOptions={{ast: true}} viewOptions={{ expr: expressionInterpreter, // 限制(或禁用)spec 可加载的内容。 // `mode: 'file'` 且不带 baseURL 时拒绝一切加载; // 要按域名白名单放行,请传入自定义 loader。 loader: loader({mode: 'file'}), }} />;

vega-interpreter是独立包(npm install vega-interpreter);同时建议配合省略'unsafe-eval'的 Content-Security-Policy,让平台层(而非仅配置层)强制这条安全边界。在 types.ts 的spec文档中,这组配置被标注为"必需而非可选"。

Schema 校验与parseSchema工具

VegaChart在开始任何工作前先校验spec.$schema,以下情况会调用onError且不渲染:

  • $schema缺失或非字符串;
  • URL 不匹配官方格式schema/{library}/{version}.json
  • 库名不是vegavega-lite

校验实现位于 schema.ts:正则SCHEMA_RE = /schema\/([\w-]+)\/([\w.-]+)\.json$/匹配 schema 路径段,且容忍任意代理前缀(任何schema/之前的内容均可)。parseSchema也从包的 barrel 入口 index.ts 作为公共工具导出,返回:

  • 成功:{ok: true, library: 'vega' | 'vega-lite', version: string}
  • 失败:{ok: false, error: string}(URL 缺失、格式错误或库未知)。

测试 schema.test.ts 给出了完整的行为矩阵:可解析vega-lite/v5vega/v6、多段版本号v5.2.0,容忍代理前缀https://internal-proxy.example.com/assets/vega.github.io/schema/vega/v5.json,拒绝缺失(并附带修复提示文案)、非字符串(报出实际类型)、格式不符与未知库(如vega-embed)。

额外导出:Astryx 主题化的 Vega-Lite 配置

VegaChartparseSchema外,index.ts 还导出了主题配置工具与常量(均源自 vegaLiteConfig.ts):

  • buildVegaLiteConfig(token):接收一个 CSS 自定义属性解析函数(如useXDSTheme()返回的token),返回一份以 Astryx token 主题化的 Vega-LiteConfig,涵盖 axis 样式、图例布局、线/点 mark 默认值、标题排版与视图 chrome——颜色标度由range结合数据可视化 token 设置;
  • 常量:DEFAULT_STROKE_WIDTH(线宽 2)、DEFAULT_POINT_SIZE(点尺寸 64)、DEFAULT_LEGEND_ORIENT'right')、LEGEND_OFFSET(16)、TITLE_OFFSET(16)。

典型用法是在可访问主题的组件内调用:const config = buildVegaLiteConfig(token),然后作为compileOptions.config或直接并入 spec 的 config。

构建

使用 pnpm workspace 命令构建(tsup 产出 CJS + ESM,随后tsc仅发声明文件):

pnpm -F @astryxdesign/vega build

tsup.config.ts 定义src/index.ts为入口,产出cjsesm两种格式,并将reactreact-domvegavega-lite声明为 external(不打包进产物,由使用方 peer 依赖提供)。产物映射在 package.json 中:main指向dist/index.jsmodule指向dist/index.mjstypes指向dist/index.d.ts

发布机制:canary 自动发布与 stable 毕业流程

为什么当前只有 canary

package.json 保持"private": true并带有"astryx": {"canaryOnly": true}标记,二者共同约束发布行为。发布工作流 .github/workflows/release.yml 同时处理两个 dist-tag:

  • stable(latest)job:跳过所有privatecanaryOnly的包(!p.private && !p.astryx?.canaryOnly才可发布),因此 Vega 永远不可能被误发布为 stable 版本;
  • canary job:每次 push 到main时触发。仅在临时 CI 检出中(绝不写回 git)为canaryOnly包剥离private标记,并以0.x.y-canary.<short-sha>版本、--tag canary --provenance --access public发布,配合 npm OIDC trusted publishing 与 provenance 供应链凭证。

提交在仓库中的private: true是 npm 层面"不可能发生 stable 发布"的硬保证——在有意执行下述毕业步骤之前不要移除它

首次 canary 的前置引导

第一个 canary 在包名被 npm 认领之前不会发布:npm 无法为一个尚不存在的名字注册 OIDC 信任。需要@astryxdesignnpm org 的 owner 做一次引导(首次 stable 发布前同样适用):

npm i -g npm@latest npm login --registry https://registry.npmjs.org # 必须是 @astryxdesign org owner pnpm run setup-trusted-publishing # 审计——显示哪些需要 bootstrap/trust pnpm run setup-trusted-publishing --bootstrap --setup-trust --workflow release.yml

这会发布一个 deprecated 的0.0.0-bootstrap.0桩版本以认领包名,并把release.yml注册为受信任发布者。在完成之前,CI 对该包的 canary 发布会失败。

毕业为公开 stable(latest)发布

当 Vega 准备好公开发布 stable 版本时,按顺序执行以下步骤(与其他@astryxdesign/*公开包的发布流程一致):

  1. 移除 canary-only 限制(编辑packages/vega/package.json):删除"private": true;删除"astryx": {"canaryOnly": true}块。

  2. 加入版本组:在.changeset/config.jsonfixed数组中加入@astryxdesign/vega,使其与其他可发布包协同版本号(统一升到同一版本),并将version设为其他包当前的已发布版本。

  3. 确认包名已在 npm 认领并建立信任(即上文引导步骤),未认领/未信任的名字在 stable 发布时与 canary 一样会失败。

  4. 添加 changeset,让发布说明与版本号包含 Vega:

    pnpm changeset:new
  5. 落地变更,然后走常规发布流程完成版本号提升与发布

    • 合并移除限制的 PR(该 push 到main会自动触发一次 canary 发布);

    • 运行版本提升 PR(pnpm version-packages,刷新 lockfile 后合并)——它会在main上提升版本号但不发布任何东西;

    • 手动派发 stable Release 工作流发布latestdist-tag:

      gh workflow run release.yml --ref main -f dry-run=true # 可选预览 gh workflow run release.yml --ref main # 发布 latest gh run list --workflow=release.yml -L 3 # 观察进度

    发布过程无 token(npm OIDC trusted publishing),且版本门控 + 幂等——重复运行是安全的,无需手写npm publish,也无需 npm token。

stable 发布完成后,npm install @astryxdesign/vega(不带 tag)将解析到 stable 版本;@canarytag 则继续跟踪main

源码结构与测试覆盖

包内文件布局(见 packages/vega):

文件角色用途
src/index.tsBarrel公共 API 出口(VegaChartparseSchemabuildVegaLiteConfig与主题常量)
src/VegaChart.tsx组件检查$schema、编译或直渲、拥有 View 生命周期
src/viewInputs.ts工具检测拥有 View 的 props 的值变化(latch/快照)
src/schema.ts工具解析并校验 Vega/Vega-Lite$schemaURL
src/types.ts类型本包共享的 TypeScript 类型
src/vegaLiteConfig.ts工具Astryx 主题化的 Vega-Lite 配置构建器
src/VegaChart.test.tsx测试View 生命周期与错误契约的功能测试
src/schema.test.ts测试$schemaURL 解析器测试
src/viewInputs.test.ts测试变更检测器单元测试

测试采用 vitest,共三个测试文件覆盖三大核心契约:spec 分支与透传(compile/parse 参数逐一断言)、View 重建策略(值比较、引用比较、循环/超深退化、data 非响应式)与错误契约$schema非法时既不 compile 也不 parse、不构造 View,仅渲染空容器 div;编译期同步异常与runAsync拒绝均路由到onError,非 Error 拒绝值会被包装为Error,卸载后迟到的拒绝不再触发回调)。这些测试既是行为的权威定义,也是你在集成 VegaChart 时最值得参考的使用边界文档。

【免费下载链接】astryxAn open source design system that's fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 17:15:00

OpenSceneGraph源码编译与开发环境搭建:从CMake到第一个Demo

写这篇东西之前&#xff0c;先交代一下背景。OpenSceneGraph&#xff08;后面统称OSG&#xff09;这套基于OpenGL的场景图渲染引擎&#xff0c;在可视化仿真、数字孪生、科学计算可视化、GIS三维展示这些领域里一直有稳定的用户群。我对它的定位一直是“够用、透明、不黑盒”—…

作者头像 李华
网站建设 2026/9/15 17:14:13

gRPC开发流程实战:从基础概念到完整服务搭建

1. 为什么我建议你一定要搞懂gRPC开发流程如果你这几年一直在写后端服务&#xff0c;肯定能感受到微服务架构已经把单体应用拆得越来越细&#xff0c;服务之间的通信方式也从简单的HTTP JSON调用&#xff0c;慢慢转向了高性能、强契约的RPC框架。gRPC就是其中绕不开的一个。我最…

作者头像 李华