加载中...

本篇要回答的问题:辅助界面元素(自定义控件)的架构和应用程序有何不同?怎么搭一个统一的控件框架让控件可替换、可包装第三方库,以及把"画图"程序本身控件化要克服哪些"唯一性假设"?

第二章"桌面软件开发"进入尾声。前面五讲用"画图"程序实战验证了应用程序的业务架构(MVC)。本讲换个对象:辅助界面元素——也就是通用控件或自定义控件(如画图程序里的线型选择器、颜色选择器)。它和应用程序的架构考虑颇有不同,最大差异在于多实例。起因是产品经理嫌 <select> 实现的颜色选择器不够友好,要换成可视化的版本。

产品经理想要的可视化颜色选择器

辅助界面元素的框架

先总结基础版控件(BaseLineWidthPicker、BaseColorPicker)的使用接口:

控件的统一使用接口(id/value/palette/blur/onchange)

接口项 含义
id 控件 id,用于获取顶层结点
value 控件的值,即 Model 层数据;控件 DOM 常简单到只是一个值(线型是 number,颜色是 Color)
palette 颜色选择器的调色板,指示可选颜色
blur() 主动让控件失去焦点
onchange 值经用户交互改变时触发;直接 element.value = xxx 赋值不会触发

框架做法:每个控件用 <div type="xxx"> 占位。引入全局 qcontrols: QControls,所有控件向它 register(type, control),把类型与构建函数关联。init() 遍历文档所有 div,凡带 type 且注册过的,就调用构建函数把占位 div 替换为真实控件。

构建函数(以 BaseColorPicker 为例)三步走:

步骤 做什么
1. 读参数 从占位 div 读 id、onchange、palette
2. 替换界面 div.outerHTML = ... 替换为真实界面(如 <select>
3. 装事件 若用户关心 onchange,把响应函数装到真实界面的 onchange 上

jQuery 颜色选择器:包装第三方库

新版 ColorPicker 基于 jQuery 社区的 spectrum 控件,使用姿势与 BaseColorPicker 完全一致,替换时只需把 type 从 BaseColorPicker 改成 ColorPicker

ColorPicker 与 BaseColorPicker 一致的使用接口

对待引入的 jQuery 有两种态度:

态度 做法 风险
作为主体框架 满屏 $,jQuery 风格代码到处蔓延 哪天弃用 jQuery,大量活跃模块都要改,代价高
仅局部使用(本讲采纳) 只在颜色选择器等少数场景用,包装隔离 控制蔓延范围,主体仍裸写 JS

包装中有个古怪点:用 Object.defineProperty 改写界面元素 value 的 get/set。原因——element.value = xxx 不触发 onchange,必须从 set 函数感知改写。而 set 里调 elem.spectrum("set", value) 内部又会写 element.value,造成死循环,靠引入 busy 标志(已在 set 中则直接返回)破解。

辅助界面元素的架构设计:为什么不用 MVC

这些控件都没有用 MVC。

关键判断:不是控件不适合 MVC,而是规模太小——DOM 只是一个 value 而非一棵树,Model 层几乎没代码,View/Control 也过于短小,没必要再做清晰的模块划分(View 管呈现、Control 管事件,心里有谱即可)。

但并非所有控件都这么简单。把整个"画图"程序改造成一个标准控件 PaintView 是可行的,关键是修正与"唯一性"有关的假设

原假设(单例) 控件化后的问题 解决方案
全局唯一 qview: QPaintView 同界面可能多个 View 实例 qview 含义从"单例"变为"当前实例"
辅助界面元素 id 固定(单例) 多个 QPaint 实例共享一套控件 保持单例,引入"当前实例"概念 + 焦点切换事件
Controller 直接 registerController 多 View 下注册时机不对 引入 onViewAdded/onCurrentViewChanged,新 View 创建时再注册 Controller
样式存在 qview.style 多 View 样式不一致、和单例控件冲突 把它挪到全局,改名 defaultStyle

改造后 newPaintView 创建实例并 fireViewAddedPaintView(div) 把占位 div 换成 <canvas> 并初始化,注册为标准控件,即可到处引用做出 DEMO。

PaintView 控件多实例 DEMO 效果

遗留问题:URL 地址。应用程序 load 文档后改 URL 没问题;但作为控件、一个界面多个 PaintView 时,URL 该显示哪个文档 ID?谁都不合适——若非显示不可,得在实例旁放个辅助元素来显示。

总结

控件的架构与应用程序"没有本质不同",唯一要额外操心的是支持多实例。多实例看似简单,实则是检验架构合理性的试金石——它会逼你消灭"全局变量满天飞"的坏习惯。即便不真的控件化,用"控件化"视角审视应用,也有助于做出更好的架构决策、形成更好的设计规范。

先把"是什么"回答清楚

概念 一句话说明
辅助界面元素 通用控件/自定义控件,DOM 常简化为单个 value
QControls 注册框架 type→构建函数的注册表,把 <div type="xxx"> 占位替换为真实控件
onchange 触发规则 仅用户交互触发;element.value= 赋值不触发,需改写 value 的 get/set
包装第三方库 把 jQuery/spectrum 限制在局部,避免风格蔓延
唯一性假设 单例代码里隐含的"全局只有一个"前提,控件化时必须逐一修正
单例→当前实例 控件保持单例,qview 改指"当前 View",配合焦点切换事件

一句话速记

控件架构和应用没本质区别,差的只是必须支持多实例;而多实例的真正障碍,是那些藏在全局变量里的"唯一性假设"——把它们一个个揪出来改成"当前实例",应用就具备了控件化的底子。

几条值得记住的判断

  • 小规模可不上 MVC:DOM 只是一个 value 时,分层是过度设计。
  • 第三方库要隔离包装:限制蔓延范围,换库成本才可控。
  • 多实例是架构试金石:能否控件化,暴露了你对全局状态的依赖程度。

思考题

PaintView 控件化后,URL 该显示哪个文档 ID 的问题留给了读者。如果同一界面有多个 PaintView 实例,你会如何设计 URL/路由与"当前实例"的关系?把你应用里某个"理所当然只有一个"的全局对象拿出来,设想它变成多实例,会有多少处代码需要改?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关