FIELD NOTE / HIGH SIGNAL
Qt 插件化导航栈:页面、路由与业务解耦
围绕 Qt 插件化导航栈设计:页面、路由与业务模块如何彻底解耦 拆出判断框架、执行路径和可复用代码片段。

一个典型的失控现场:页面 A 直接创建页面 B,页面 B 又持有业务模块 C;返回时,三个对象都在修改同一份状态。最终,导航顺序、对象生命周期和业务数据被绑成一团。
问题通常不在 QWidget 或 QML 本身,而在于系统没有明确回答三件事:谁决定去哪里、谁创建页面、谁拥有业务状态。
先看目标架构:依赖只向内收敛
插件化导航栈的核心,不是把页面塞进动态库,而是建立稳定的依赖方向。页面、路由和业务模块分别承担展示、调度与能力提供,三者通过最小契约协作。
推荐的五层结构
01Shell:持有窗口、导航容器和全局 UI 框架。02Navigation Core:维护路由表、导航栈和页面生命周期。03Page Contract:定义页面工厂、参数和返回结果。04Business Plugin:注册路由,并提供业务服务与页面工厂。05Infrastructure:提供日志、存储、网络和事件总线等基础能力。依赖规则:Shell 只认识导航接口,导航核心只认识页面契约,业务插件只在注册阶段暴露能力。任何页面都不应直接包含另一个业务模块的具体类。

第一层:让路由成为稳定契约
很多项目把路由写成字符串,然后在各处调用 createWidget。这样虽然能跳转,却没有解决参数类型、返回值和路由归属问题。更稳妥的做法,是把一次导航定义为一份完整请求。
02parameters:进入页面时的不可变参数快照。03presentation:push、replace、modal 等展示策略。04resultChannel:页面关闭后返回结果的通道。下面是一组可落地的 C++ 接口。导航核心不依赖具体页面,只处理请求、工厂和页面句柄。
10 QVariantMap parameters;11 enum class Presentation { Push, Replace, Modal } presentation17 virtual ~IPageContext() = default;18 virtual const QVariantMap& parameters() const = 0;19 virtual void finish(const QVariantMap& result = {}) = 0;22using PageFactory = std::function<QWidget*(IPageContext&, QWidget*)>;26 using ResultHandler = std::function<void(const QVariantMap&)>;27 virtual ~IRouter() = default;28 virtual bool registerRoute(const QString& routeId,29 PageFactory factory) = 0;30 virtual bool navigate(const RouteRequest& request,31 ResultHandler onResult = {}) = 0;32 virtual bool back(const QVariantMap& result = {}) = 0;关键变化:调用方只表达“我要进入某个能力”,不再负责构造页面,也不需要知道该页面属于哪个插件。
第二层:页面工厂隔离创建与生命周期
页面不应由调用方 new,也不应长期存放在全局单例中。导航核心在执行路由时调用工厂,创建页面上下文和页面实例,再把二者作为同一个栈帧管理。

业务插件只在加载阶段注册工厂。工厂内部可以获取本插件的服务,但页面创建完成后,导航核心不需要理解这些服务。
01class OrderPlugin final : public QObject {03 bool start(IRouter& router, OrderService& service) {04 return router.registerRoute(05 QStringLiteral("order/detail"),06 [&service](IPageContext& context, QWidget* parent) {08 context.parameters().value("orderId").toString();09 return new OrderDetailPage(10 orderId, service, context, parent);16request.routeId = QStringLiteral("order/detail");17request.parameters.insert("orderId", QStringLiteral("A1024"));19router.navigate(request, [](const QVariantMap& result) {20 const bool changed = result.value("changed").toBool();需要避免:工厂返回页面后,插件再私下保存页面裸指针。否则导航核心销毁页面时,插件仍可能访问已经失效的对象。
第三层:用状态边界解决多页面冲突
页面状态冲突通常来自两类混用:把“业务真相”放进页面,把“页面草稿”放进全局服务。解法不是增加更多信号,而是先划定状态所有权。
01业务状态:订单、用户、权限等事实数据,由业务服务持有。02导航状态:当前路由、参数、栈顺序,由导航核心持有。03页面状态:滚动位置、选中项、未提交表单,由当前栈帧持有。04跨页结果:通过 finish 返回,不通过直接修改上一个页面。推荐的数据流
页面读取参数 → 调用业务服务 → 在本地编辑草稿 → 提交服务 → 通过结果通道通知上一页刷新。上一页根据业务标识重新查询,而不是接收另一个页面的内部对象。
如果页面需要恢复,可让页面实现独立的状态序列化接口,由导航核心保存 QVariantMap。这样恢复机制仍然不需要认识具体控件。
第四层:插件卸载必须服从导航栈
动态插件最危险的时刻不是加载,而是卸载。只要栈里还有该插件创建的页面、工厂闭包或回调,卸载动态库就可能留下悬空函数地址。
工程上可以把插件状态设计为 Active、Draining、Stopped。进入 Draining 后拒绝新页面,只处理存量页面退出。相比直接 unload,这种两阶段关闭更容易验证,也更适合存在异步任务的桌面应用。
测试重点:同一路由重复进入、页面中途返回、异步回调晚到、插件卸载时仍有页面、replace 后旧页面销毁、返回结果无人接收。
结尾:按四步改造现有项目
不要从拆动态库开始。先把导航关系从页面代码中抽离,再逐步建立插件边界。
01第一步:统计所有页面跳转,删除页面之间的直接 new 和具体类引用。02第二步:建立 IRouter、RouteRequest、IPageContext 和 PageFactory 四个最小契约。03第三步:让导航核心统一持有页面栈帧,并明确业务、导航、页面三类状态的所有权。04第四步:最后再接入插件注册、停用与卸载流程,并补齐生命周期测试。验收标准:任意业务插件被移除后,其他模块仍能编译;任意页面被替换后,调用方无需修改;任意一次返回都通过结果契约完成;任意状态都能明确指出唯一所有者。做到这四点,页面、路由与业务模块才算真正解耦。
阅读建议:先看每节标题,再回到代码块或清单部分直接执行。