3. 组件化设计思想

在 QML 界面架构中,组件化与模块化经常被混用,但二者关注的层次并不相同。理解它们的差异,是建立可扩展 QML 工程的第一步。组件化关注“界面单元如何封装与复用”,模块化关注“工程单元如何划分与依赖”。在 QML 中,一个 Button.qml 是组件,而一个包含按钮、弹窗、主题、工具函数的 ui-core 目录则更接近模块。

一、组件化与模块化的本质区别

组件化是以可复用 UI 单元为中心的拆分方式。它强调一个 QML 文件或一组 QML 文件对外暴露清晰的属性、信号、方法和默认插槽,内部实现可以替换而不影响调用方。模块化则是以工程边界为中心的拆分方式,它通过目录、qmldir、命名空间、构建目标或包管理来组织一组相关组件与资源。

换句话说,组件化解决“这个按钮能不能在十个页面里复用”,模块化解决“这十个页面和按钮应该放在哪个工程目录、由谁依赖、如何发布”。二者不是替代关系,而是不同粒度的协作关系:模块通常由多个组件构成,组件通过模块获得稳定的导入路径与版本边界。

组件化与模块化的层次关系 模块提供工程边界,组件提供界面复用单元,页面通过导入组合二者 模块层 Module:ui-core qmldir · 命名空间 · 版本边界 · 资源与主题 · 构建目标 基础控件组件 Button / Input / Card 反馈组件 Dialog / Toast / Loading 主题与样式 Theme / Palette / Fonts 工具与单例 Utils / Logger / Config import ui-core 1.0 组件层 Component:可复用界面单元 对外属性 · 信号 · 方法 · 默认插槽 · 内部实现可替换 PrimaryButton.qml SearchBar.qml UserCard.qml 页面层 Page:组合组件,不重复实现细节 LoginPage.qml · UserListPage.qml · SettingsPage.qml

二、高内聚低耦合原则

高内聚指一个组件内部只做一件事,并且把这件事做完整。例如 SearchBar.qml 只负责输入、清空、提交搜索信号,不负责请求网络,也不负责跳转页面。低耦合指组件之间尽量通过属性、信号和接口通信,而不是直接访问彼此的 id 或内部对象。

在 QML 中,低耦合最常见的做法是:父组件通过属性向下传数据,子组件通过信号向上通知事件。避免在子组件中写 parent.parent.color = "red" 这类依赖外部结构的代码,因为一旦布局层级变化,组件就会失效。

// 推荐:通过属性与信号解耦
// SearchBar.qml
Item {
    id: root
    property string placeholder: "搜索"
    signal submitted(string keyword)

    TextField {
        id: input
        placeholderText: root.placeholder
    }

    Button {
        text: "搜索"
        onClicked: root.submitted(input.text)
    }
}

// 调用方只关心接口,不关心内部结构
SearchBar {
    placeholder: "搜索用户"
    onSubmit: userModel.search(keyword)
}

三、可复用性与可维护性目标

可复用性不是“写一次到处粘贴”,而是“写一次,通过参数适配不同场景”。一个可复用组件通常具备以下特征:属性可配置、信号可扩展、样式可覆盖、默认行为合理、不依赖具体业务数据。可维护性则要求组件边界清晰、命名一致、依赖方向单一、修改影响范围可控。

维度 组件化关注点 模块化关注点 可复用/可维护目标
拆分单位 QML 文件或组件组 目录、qmldir、包 按职责拆分,避免上帝组件
对外接口 属性、信号、方法、插槽 import 路径、版本、命名空间 接口稳定,内部可替换
依赖方向 父到子传数据,子到父发信号 页面依赖模块,模块不依赖页面 单向依赖,避免循环引用
复用方式 参数化配置、样式覆盖 跨工程导入、版本发布 一次实现,多场景适配
维护成本 修改内部不影响调用方 模块升级有边界与记录 影响范围可预测

四、案例场景:从“复制粘贴”到“组件复用”

假设一个 QML 项目中有三个页面都需要用户卡片:登录页显示当前用户,列表页显示用户摘要,设置页显示账户信息。初期做法是每个页面各写一份 Rectangle + Image + Text,结果头像圆角、字体大小、点击行为都不一致。重构时抽取 UserCard.qml,对外暴露 userName、avatarUrl、subtitle 和 clicked() 信号,三个页面通过属性传入不同数据,样式由主题模块统一控制。

重构收益:用户卡片从 3 份重复代码变为 1 个组件;修改头像圆角只需改一处;新增“访客卡片”只需传入不同属性,不需要复制文件。

五、常见误区与复盘

  • 误区一:把“文件拆分”当成组件化。拆成很多 QML 文件,但每个文件仍然直接访问全局对象和外部 id,耦合并没有降低。
  • 误区二:组件属性过多。一个组件暴露 20 多个属性,说明职责不单一,应该继续拆分或使用配置对象。
  • 误区三:模块边界模糊。页面直接 import 另一个页面的私有组件,导致模块之间形成网状依赖,难以独立发布。
  • 误区四:过度抽象。只有一处使用的界面也强行抽成组件,增加跳转成本,反而降低可读性。

实践建议:先按“是否会被复用”和“是否有独立职责”判断是否抽取组件;再按“是否会被其他工程或团队依赖”判断是否升级为模块。组件接口保持窄而稳定,模块依赖保持单向且可追踪。

组件化与模块化共同服务于同一个目标:让 QML 界面在需求变化时,修改范围可控、复用成本低、协作边界清晰。高内聚让每个单元自身完整,低耦合让单元之间互不拖累,可复用性与可维护性则是衡量拆分是否合理的最终标准。