生态产品多个设计器现已发布正式版

查看生态总览
跳转到内容

诊断通道 ​

组件在运行期发现契约问题时不直接 console.warn,而是向诊断通道投递一条结构化记录。宿主订阅后自行决定打印、收集还是上报。

不直接打日志的原因 ​

契约违约需要被处理,不只是被看见。wc.missing-part 这类问题在开发时应明显报出,在测试中应使用例失败,在生产环境应静默:同一条记录,三种归宿。写死 console.warn 只满足第一种。

记录的形状 ​

ts
interface DiagnosticRecord {
  code: string; // 稳定标识,如 'wc.missing-part'
  level: "error" | "warn";
  message: string; // 面向开发者的说明,文案可变
  scope?: string; // 组件名,取解剖名
  instanceId?: string; // 区分同一组件的多个实例
  part?: string;
  node?: Element;
  detail?: Record<string, unknown>;
}

code 稳定,message 不稳定。订阅方按码分流,不匹配文案。

现有的码 ​

ts
export const DIAGNOSTIC_CODES = {
  invariant: "core.invariant", // 断言不成立
  warn: "core.warn", // 条件告警
  layerDisposeNotTop: "core.layer.dispose-not-top", // dispose 的层上面还有开在它里面的层
  machineError: "machine.error", // 机器抛出 MachineError
  wcMissingPart: "wc.missing-part", // 作者未渲染必需的角色节点
  wcUnknownPart: "wc.unknown-part", // 角色节点的 part 名不在组件解剖内
  wcWrongPartTag: "wc.wrong-part-tag", // 角色节点的标签不满足要求,原生语义会静默失效
  wcStructureTakeoverOrder: "wc.structure-takeover-order", // 标准结构里可接管的部件没有排在 root 的末尾
  wcUnregisteredElement: "wc.unregistered-element", // 标准结构生成了一个还没注册的库内元素
  designerExtensionConflict: "designer.extension-conflict", // 设计器扩展在两处登记了同一个名字,给 extensions 赋值时抛错,不在诊断通道里报
  matrixCodeLogoDamage: "matrix-code.logo-damage", // 中心 logo 挖掉的码字超出纠错级别能恢复的量
  matrixCodeOptionIgnored: "matrix-code.option-ignored", // 二维码收到一个对当前码制没有意义的选项,按没给处理
  barCodeOptionIgnored: "bar-code.option-ignored", // 条形码收到一个对当前码制没有意义的选项,按没给处理
  progressOptionIgnored: "progress.option-ignored", // 进度条收到一个对当前形态或语义没有意义、或取值不合法的选项,按没给处理
  stepsOptionIgnored: "steps.option-ignored", // 步骤条收到一个对当前形态没有意义、或取值不合法的选项,按没给处理
  carouselSlideCountMismatch: "carousel.slide-count-mismatch", // 轮播渲染出来的条目比 slideCount 多:张数只看 slideCount,多出来的那几张翻不到
  stylesMissingSkin: "styles.missing-skin", // 页面上出现了组件,但它那份皮肤没被引入
  versionMismatch: "core.version-mismatch", // 适配器与 core 的版本不一致,锁步发版被打破
  ignoredSlot: "core.ignored-slot", // 作者给了默认插槽,但该组件不渲染插槽内容
  overlayStackingTrap: "overlay.stacking-trap", // 浮层的祖先建了层叠上下文,浮层的层号被困在其中
  scrollbarMissingScrollable: "scrollbar.missing-scrollable", // 滚动条挂载时找不到它要管的滚动容器
  overlayMissingAnchor: "overlay.missing-anchor", // 浮层展开了却没有锚点,位置无从算起
  portalUnreadableStylesheet: "portal.unreadable-stylesheet", // 有跨域样式表没以 CORS 加载,Portal 视觉桥的样式索引作废,浮层同步退回整表枚举
  chartMissingName: "chart.missing-name", // 图表没有可及名:caption 部件、aria-label、aria-labelledby 都没有
  chartUnknownField: "chart.unknown-field", // 系列引用的字段在数据里不存在
  chartDuplicateSeries: "chart.duplicate-series", // 两个系列的 id 相同
  chartTooManySeries: "chart.too-many-series", // 分类系列超过 8 个:没有第 9 色
  chartMixedColorRoles: "chart.mixed-color-roles", // 同一张图混用分类色与语气色
  chartInvalidSlot: "chart.invalid-slot", // 固定色槽越界,或两个系列固定到同一槽
  chartStackOffsetConflict: "chart.stack-offset-conflict", // 同一堆叠组的 stackOffset 不一致
  chartLogDomain: "chart.log-domain", // 对数轴的定义域含 0 或跨越正负
  chartScaleParam: "chart.scale-param", // 坐标轴的参数无效:幂指数、对称对数常数、时区或最小厚度
  chartBarBaseline: "chart.bar-baseline", // 柱系列所在的值轴不含 0
  chartNegativeShare: "chart.negative-share", // 占比类图表出现负值
  chartInvalidRange: "chart.invalid-range", // 区间不合法:两端不是有限数,或下界大于上界
  chartMeterOnly: "chart.meter-only", // Progress 在非 meter 语义下用了分段、目标、量程刻度或指示方式
  chartAnnotationTarget: "chart.annotation-target", // 图表注释指向的系列不存在,或指向的类目不在轴上:这条注释不画
  chartOhlcRange: "chart.ohlc-range", // K 线的开高低收对不上:最低价高于开盘或收盘,或最高价低于开盘或收盘
  chartViolinRaw: "chart.violin-raw", // 小提琴图要原始值才画得出密度:箱线系列写了 style: 'violin',y 却是算好的五数字段
  chartIndicatorCount: "chart.indicator-count", // 雷达图的指标少于 3 个或多于 10 个:围不成面,或轴挤在一起读不出来
  chartRadarOverlap: "chart.radar-overlap", // 雷达图的实体多于 3 个:多边形互相遮挡,按两两配对检查只有前 3 个色槽都合格;图照常画,按提醒报
  chartHierarchyShape: "chart.hierarchy-shape", // 层级图的数据组不成一棵树:扁平的行缺 idField / parentField,或 id 重复、父节点不存在、多个根、成环
  chartSankeyShape: "chart.sankey-shape", // 桑基图的流带不合法:成环、自环、端点不存在或节点重复;负值另报 chart.negative-share
  chartGraphShape: "chart.graph-shape", // 关系图的数据不合法:节点重复、连线的端点不存在、自环,或树布局下数据不是一棵树
  chartGraphSize: "chart.graph-size", // 关系图的节点太多:多于 500 个时交互变慢,按提醒报;多于 2000 个时不画,先聚合
  chartColumnsOption: "chart.columns-option", // 列式数据用了它不支持的写法(堆叠、瀑布、箱线、类目轴、横向、逐点标签、刷选……),或与 svg 渲染器同写
  chartColumnsUnsorted: "chart.columns-unsorted", // 列式数据里折线、K 线与柱共用的自变量列不是升序(追加了乱序的时间戳)
  chartOptionConflict: "chart.option-conflict", // 两种写法不能同时用:柱的涨跌取色(trend)与瀑布;等距排列(xAxis.ordinal)与对象数组
};

这份清单与 @xihan-ui/core 中的码表逐条对账,不会遗漏,可以直接据此编写分流。

machine.error 的 detail.machineCode 是状态机错误码。机器崩溃或正常停机清理失败时,detail.reason 保留实际上报的原始或聚合异常对象;同时发生 cleanup 与 exit 异常时,它与调用方捕获的 AggregateError 是同一个对象,可以继续读取 errors 与 cause。

状态机实现引用属于执行前置条件。UNKNOWN_ACTION、UNKNOWN_GUARD、UNKNOWN_EFFECT 在创建机器时拒绝静态缺项;动态列表或实现内部引用缺项时使用 MISSING_ACTION、MISSING_GUARD、MISSING_EFFECT。后三种错误在开发与生产都会先投递 machine.error;没有其他停机异常时,其 detail.reason 与随后抛出的 MachineError 是同一个对象,存在其他停机异常时则由后续崩溃记录携带聚合结果。服务同时进入 Stopped。诊断阈值只控制记录是否送达,不会把错误改成继续执行、guard 的 false 或部分 effect。

三条 wc.* 是 Web Components 适配器的部件契约校验,也是日常最常遇到的三条:手写 DOM 时遗漏一个 data-xh-part 或写错名字,通道会明确指出节点与部件。

用法 ​

ts
import { onDiagnostic, setDiagnosticsConsoleOutput, setDiagnosticsLevel } from "@xihan-ui/core";

// 订阅
const off = onDiagnostic((record) => {
  if (record.code === "wc.missing-part")
    reportToSentry(record);
});

// 调阈值:'error' | 'warn' | 'silent'
setDiagnosticsLevel("warn");

// 关闭内建 console 输出,只使用自己的订阅
setDiagnosticsConsoleOutput(false);

其余可用的接口:

函数作用
getDiagnostics()获取通道对象本身
reportDiagnostic(record)投递一条(自定义组件中使用)
setDiagnosticsDedupe(on)同一 code + scope + instanceId + part + message 是否只报一次
resetDiagnostics()清空订阅者、去重记录与全部开关

默认行为 ​

环境阈值console 输出去重
开发warn开开
生产silent关开

生产默认 silent,投递直接被丢弃,不产生任何开销。需要在生产收集时,显式调用 setDiagnosticsLevel('error') 并挂载自己的订阅。

去重键包含 message:同一个码下不同文案是不同的问题,只有逐帧重复的同一条才应被压缩。去重集有容量上限,超出即整体清空,长时间运行的进程不会无界增长。

隔离性 ​

  • 通道挂在全局,同一页面中多份 @xihan-ui/core 副本共用一条通道;
  • 订阅方抛错不会回流进组件:上报逻辑出错不影响界面。

在测试中使用 ​

把阈值调到 warn 并订阅,即可把契约违约变为用例失败:

ts
import { onDiagnostic, resetDiagnostics, setDiagnosticsLevel } from "@xihan-ui/core";

beforeEach(() => {
  resetDiagnostics();
  setDiagnosticsLevel("warn");
});

it("不应有契约违约", () => {
  const records: DiagnosticRecord[] = [];
  onDiagnostic(r => records.push(r));
  render();
  expect(records).toEqual([]);
});

相关 ​

Released under The MIT License