Blazor Developer Tools v0.10:深入探讨框架级集成
Source: Dev.to
Introduction
当我首次发布 Blazor Developer Tools 时,我的目标是让 Blazor 开发者拥有与 React 开发者多年来一直享受的组件检查体验相同的功能。React DevTools 能让你看到组件树、实时检查 props,并了解应用的结构。而 Blazor 当时并没有等价的工具。
v0.9 版本是一个可工作的 MVP,但它的架构存在根本性的局限,最终会碰壁。经过数周研究 Blazor 源码并探索各种死路,我提出了一套全新的 v0.10 架构,从框架层面解决了这些问题。
本文将说明我的收获、考虑过的选项以及为何新方法可行。
v0.9 Architecture
v0.9 的架构很巧妙,但本质上是一个变通方案。它包括三个步骤:
- 构建时转换 – 一个 MSBuild 任务扫描你的
.razor文件,创建影子副本,并注入带有组件元数据的不可见<span>标记。 - 运行时检测 – 浏览器扩展扫描 DOM 中的这些标记。
- 树重建 – 扩展根据标记的位置重新构建组件层级。
<!-- injected marker example (empty in original) -->
Problems with v0.9
-
组件库冲突 – 某些库(例如 MudBlazor)会验证它们的子元素,当出现意外元素时会抛出异常。注入的 span 会导致此类失败:
<!-- example of problematic injected markup --> <span data-bdt-component-id="123" style="display:none;"></span> -
需要变通 – 我添加了
SkipComponents配置,让用户可以排除有问题的组件,但这只是补丁而非根本解决方案。用户必须自行发现哪些组件会出错并进行相应配置。 -
静态元数据 – span 标记中保存的是构建时捕获的元数据,和实际运行的组件实例没有关联。因此:
- 没有实时的参数值。
- 没有性能指标(渲染次数、耗时)。
- 没有组件状态——这些标记只是死 HTML,和活跃的 Blazor 应用脱节。
-
DOM 污染 – 注入不可见元素感觉不妥。它可能影响 CSS 选择器、自动化测试或边缘情况。
Desired Capabilities
从本质上讲,新架构需要能够为任意 DOM 元素回答两个问题:
- 是哪一个组件渲染了该元素?
- 该组件当前的参数值是什么?
Blazor 并未公开 API 来回答这两个问题。组件树存在于 .NET 内存中,DOM 存在于浏览器中,两者之间没有公共桥梁。
Blazor Rendering Pipeline
渲染流程的简化视图:
┌─────────────────────────────────────────────────────────────────┐
│ .NET SIDE │
│ │
│ Component Instance │
│ │ │
│ ▼ │
│ BuildRenderTree() → RenderTreeFrames │
│ │ │
│ ▼ │
│ Renderer assigns componentId, diffs against previous tree │
│ │ │
│ ▼ │
│ RenderBatch (binary format) │
│ │
└─────────────────────────────────────────────────────────────────┘
│
│ SignalR (Server) or
│ Direct call (WebAssembly)
▼
┌─────────────────────────────────────────────────────────────────┐
│ BROWSER SIDE │
│ │
│ blazor.server.js / blazor.webassembly.js │
│ │ │
│ ▼ │
│ BrowserRenderer interprets RenderBatch │
│ │ │
│ ▼ │
│ DOM mutations applied │
│ │
└─────────────────────────────────────────────────────────────────┘
关键洞见: Blazor 在内部维护了 componentId(分配给每个组件实例的整数)与其 DOM 位置之间的映射。该映射存在于 JavaScript 运行时的 BrowserRenderer 中,但它是私有的。如果我能够拦截组件创建并将其与 JavaScript 所看到的内容关联起来,就能得到所需的桥梁。
Approaches That Didn’t Work
1. Piggyback on CSS Isolation
Blazor 已经为元素添加了空的 b-xxxxxxxxxx 属性用于作用域 CSS。我曾想是否可以复用这一机制。
- 问题: Razor 编译过程是一个黑箱。CSS‑isolation 属性是编译器本身添加的,而不是通过可扩展的管道,所以没有钩子可以注入额外属性。
2. Require a Custom Base Class
强制所有组件继承自 BdtComponentBase 而不是 ComponentBase。
public class Counter : BdtComponentBase // instead of ComponentBase
{
// …
}
- 用户阻力: 要求每个组件都做修改是不可接受的。
- 继承冲突: 许多项目已经使用自定义基类或库提供的基类。
- 第三方组件: 你无法修改 MudBlazor、Radzen 或其他外部组件。
3. Source Generator to Inject Registration Code
生成代码,在组件生命周期的某个阶段(例如 OnInitialized)注册组件。
// Generated code injected into component
protected override void OnInitialized()
{
BdtRegistry.Register(this);
base.OnInitialized();
}
- 已有覆盖: 如果用户已经覆盖了
OnInitialized,会产生冲突。包装用户的实现会引发调用顺序的问题。 - Partial‑class 复杂度: Razor 组件本身已经是带有生成代码的 partial 类;再添加生成的生命周期覆盖会导致脆弱的交互。
- 边缘情况: 有些组件会覆盖
SetParametersAsync且不调用base,或者根本不继承自ComponentBase,这些都会使该方案失效。
4. Modified Blazor SDK (The “Nuclear” Option)
创建一个包含跟踪仪表的自定义 Blazor SDK。
- 维护负担: 每一次 Blazor 新版本发布都需要把改动合并到自定义 SDK 中。
- 用户阻力: 开发者需要引用非标准的 SDK。
- 信任问题: 替换核心框架组件对大多数团队来说是难以接受的。
Conclusion
v0.9 的架构证明了组件检查是可行的,但向 DOM 注入静态标记既脆弱又受限。通过了解 Blazor 内部的 componentId 映射并在框架层面拦截组件创建,v0.10 建立了一个稳健、实时的桥梁,将 .NET 组件实例与其渲染的 DOM 元素关联起来——从而实现参数、状态和性能指标的实时检查,而无需污染 DOM 或对用户代码进行侵入式修改。