一个组件可以在共享程序集中编译,但在渲染它的某个应用中却无法构造。
当浏览器和 .NET MAUI 宿主复用相同的 Blazor 页面时,这一问题很容易被忽略。该特性看似只存在一次,但每个宿主仍然拥有各自的依赖注入容器、启动路径和生命周期模型。
最近提交的一项修复让这一问题变得具体化:一个共享页面新增了一个必需的工作流依赖。原生宿主在公共引导路径中注册了它,而浏览器宿主通过另一条组合根渲染同一页面时,该依赖是未知的。页面在渲染前就失败了。
持久的教训并非“多记一次注册”,而是共享 UI 为每一个能够渲染它的宿主创建了一份契约。
共享程序集并不共享容器
编译时复用问的是多个项目能否引用一个组件;运行时组合问的是每个宿主能否构造它并满足所需行为。
原生宿主可能会调用一个包装器引导,而浏览器宿主则调用面向 Web 的启动路径。两者加载相同的 Razor 组件,却不会构建相同的服务图。
这产生了一条有用的审查规则:
共享组件上的必需注入,对每一个能渲染它的组合根而言都是一份必需契约。
宿主清单很重要。“所有原生应用都使用此引导程序”不足以覆盖浏览器、桌面、测试或预览宿主通过其他路径渲染页面的情况。
不要用可选性来弥补缺失的宿主能力
当某个宿主无法提供依赖时,将其设为可空看起来很实用。这对触觉反馈等可选增强是合理的,但当依赖强制执行工作流不变式时就很危险了。
在所审查的变更中,工作流需要知道操作进行期间运行时上下文是否保持不变。原生应用可以在运行时更改该上下文,而浏览器宿主则不能——其上下文在应用生命周期内是固定的。
可选性会将“此宿主以不同方式表示稳定性”转化为“此宿主可以跳过安全检查”。这两者并不等价。
更好的问题是:工作流真正需要的最小能力是什么?
它只需要三个事实:
- 标识当前上下文的修订号;
- 该上下文是否已准备好工作;
- 上下文已更改的信号。
它不需要原生宿主完整的导航、存储或 UI 服务。一旦更小的契约明确,两个宿主都能如实提供。
固定与实时适配器可以满足同一不变式
浏览器实现可以是固定的:
- 其修订号永不改变;
- 应用启动后即准备就绪;
- 其更改信号永不触发。
原生实现可以是实时的:
- 其修订号来自运行时上下文;
- 就绪状态反映是否存在选择;
- 其信号跟随真实的上下文变化。
它们行为不同,却遵守同一承诺:在一个上下文中开始的工作不得在该上下文更改后静默提交。抽象命名的是每个平台都能遵守的最小不变式,而不是抹除平台差异。
在宿主实际进入的位置注册契约
最初的注册位于原生应用共享的引导程序中。它看起来是中心的,因为多个宿主调用它,但它并非每个渲染页面的宿主的中心。
修复将共享工作流契约移到了两条实际的组合路径中:
- 浏览器路径注册固定适配器;
- 原生路径注册实时适配器;
- 两者都注册必需的共享工作流;
- 原生专用的包装器不再是意外的权威。
重复的注册记录了一个真实的分裂:相同的工作流,不同的宿主事实。公共注册仍然适合相同的服务;宿主特定的适配器应在宿主选择它们的位置保持可见。
测试行为与组合
注册测试很有用,因为此故障发生在组件行为开始之前。
一个聚焦矩阵可以覆盖:
- 浏览器组合根包含共享工作流和固定适配器;
- 原生组合根包含共享工作流和实时适配器;
- 旧的平台专用引导程序不是隐藏的第三方权威;
- 当宿主具有真正固定的上下文时,工作流成功;
- 当实时上下文在操作中途更改时,工作流拒绝或修复完成。
描述符检查可快速捕获缺失的注册。行为测试证明适配器保留了不变式。两者都不激活完整的生产宿主。对于高价值页面,添加一个烟雾测试,构建每个宿主的真实提供程序并构造页面边界;这可以暴露描述符检查遗漏的生命周期或替换错误。
权衡
窄契约方法会带来一个接口、两个适配器、显式注册和更宽的测试矩阵。
替代方案仅在局部更廉价:
- 宽泛的原生服务会将平台关注点泄漏到共享工作流代码中;
- 可选依赖可能静默禁用安全不变式;
- 一个过大的引导程序隐藏了哪个宿主拥有哪种行为;
- 临时检查会在没有命名契约的情况下漂移。
我更喜欢在组合边界进行小而有意的重复,而不是在工作流内部隐式行为差异。
实用的审查清单
当共享 Blazor 组件获得必需依赖时,请询问:
- 哪些浏览器、原生、桌面、测试和预览宿主可以渲染它?
- 每个宿主通过哪个组合根进入?
- 该依赖是工作流需要的能力,还是它恰好接收的大型平台服务?
- 每个宿主都能提供真实、非可选的实现吗?
- 宿主差异是否在适配器中可见,而不是在空分支中?
- 测试是否针对每个根都练习了注册和行为?
- 完整提供程序烟雾测试能否捕获生命周期或替换错误?
共享 UI 不是共享运行时拓扑的证据。将每个必需依赖视为跨宿主的设计决策,而组合根成为组件真实 API 的一部分。
你系统中的哪个共享页面正在悄悄假设它只有一个宿主?
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.