写代码的人都有一种朴素的惰性:优先复用现成的,避免重复造轮子。
当初图扑软件计划从零搭建专属 Web UI 组件库,内部最先提出质疑的,正是我们团队自己。
本文会讲清这支“画布之笔”为何必须自研,再附上极简示例代码,带你上手实操。
01 先把你的白眼接住
我猜你看到“又一套 UI 组件库”这几个字,心里已经泛起抵触。
「市面上前端组件库难道还不够多?按钮、输入框、表格、下拉框开源方案随处可见,随便挑一个成熟方案不香吗?图扑何必重新开发?」
我们十分理解这份疑问,其实立项之初,我们内部也有同样的顾虑。
如果现有组件能满足需求,没人愿意从头打磨文本录入这块硬骨头。光标闪烁、文本选中、中文输入法候选框,任意一处细节,都足够耗费大量开发精力。(剧透一下:这类复杂底层能力 HT UI 并未全部自研,后文会讲到框架如何巧妙兼容原生能力。)
但痛点,恰恰藏在大家总想复用现成方案、避免从零开发的固有思路里。
市面上绝大多数 Web UI 库,本质上都是在 DOM 这套基础架构上叠加封装。
单个按钮对应一个 <button> 标签,一张表格由上千个 <td> 组成,一棵树形组件是层层嵌套的 <div>。这类库在常规表单后台中表现流畅,可如果你的项目并非简单管理系统,而是重型工具软件、低代码可视化搭建平台、在线 IDE、十万行级别的大型表格与树形结构、属性面板密集的专业编辑器这类产品,界面上同时存在数百上千个控件时,DOM 这套架构就会彻底承压过载。
02 重场景里,DOM 是怎么累垮的
深耕这类项目的开发者,大概率都踩过这些坑:
- 低代码搭建编辑器场景
左侧组件树、中间画布主体、右侧整列属性面板,数百个控件同屏渲染。传统 DOM 方案中,每个控件都对应一组嵌套标签,浏览器仅维护这些节点的位置、样式、层级关系,就会占用大量 CPU 资源,极易出现设备风扇高速运转、页面卡顿的情况。 - 组件密集型工具软件场景
工具栏、多页签、可拖拽面板、可拉伸分割条层层叠加。用户切换页签、拖拽面板、拉伸分割条的每一次操作,都会触发大范围的 DOM 回流(reflow)与重绘(repaint),操作拖拽延迟、画面卡顿掉帧,交互体验极差。 - 十万行级树表格场景
仅将海量 <tr> 节点插入 DOM,就足以让浏览器严重卡顿,更无法保障流畅的滚动、展开、折叠交互效果。
这些场景的共性十分鲜明:元素数量多、状态变化频繁、且要求交互极致流畅。
DOM 天生适配“结构稳定、节点数量可控”的文档类页面。强行用它承载控件密集、交互频繁的重型界面,就好比让擅长写楷书的老先生去刷大面积墙面——不是能力不足,而是工具用错了场景。
图扑深耕专业工具软件研发多年,最常遇到的就是这种“控件密集、交互高频”的复杂界面场景。被实际业务场景倒逼之后,我们得出了一个简单直接的结论:
既然 DOM 堆叠的方式扛不住,那就直接改用 Canvas 绘制。
03 所谓“纯 Canvas”,到底纯在哪
我们以一个普通按钮为例,拆解两种实现方案的差异。传统 DOM 实现的按钮,结构十分繁琐:
一层 <button> 外层标签、多层 <span> 嵌套承载文字、大量 CSS 控制样式、搭配伪元素实现图标效果……仅仅一个按钮,就需要浏览器维护一整棵小型 DOM 子树。一千个按钮,就是一千棵独立的 DOM 子树。
而 HT UI 的实现逻辑完全不同:组件的所有视觉样式——边框、背景、文字、图标、选中态、禁用态,全部通过 Canvas 画笔实时绘制生成。打开 Chrome 开发者工具的 Elements 面板查看 HT UI 按钮,看不到繁杂的嵌套标签,只有极简清晰的几层节点:
- View(一个 <div>)
组件最外层容器,负责页面占位与交互事件接收,通过 button.getView() 即可获取该节点; - RootCanvas(一块 <canvas>)
组件所有可视化内容,包括边框、背景、文字、图标,全部绘制于此; - ContentDiv / ScrollBar / DisabledDiv
分别为内容容器、滚动条、禁用遮罩,按需渲染、不冗余占位。
一句话定义这份“纯粹”:仅保留极简 DOM 壳子负责交互事件,所有视觉渲染工作全部交由 Canvas 承载。
这套方案直接将页面 DOM 节点数量缩减一个量级,大幅减轻浏览器渲染负担。这并非炫技,而是复杂密集界面场景倒逼出的最优解决方案。
这里必须坦诚划定技术边界(还记得开篇提到的剧透吗?):唯一的例外是文本录入类组件。输入框、文本域等需要手动录入内容的组件,会嵌套原生 <input> / <textarea> 标签,专门承接光标渲染、文本选区、中文输入法交互等能力。这类组件的边框、背景、文字展示等视觉样式,依旧由 Canvas 绘制,真正承接键盘输入交互的,是原生 DOM 元素。
为何不全程纯 Canvas 实现?因为输入法(IME)是操作系统级的底层能力,强行用 Canvas 模拟光标、输入法候选框,不仅开发成本极高、兼容性差,还无法达到原生交互体验,得不偿失。
懂得适时取舍、精准妥协,也是技术研发的一种修养。除文本录入场景外,HT UI 所有组件的视觉效果,均由 Canvas 纯手绘实现。
基于 HT UI 搭建的数据中台操作界面:大量表格、图表、控件同屏,纯 Canvas 绘制下依旧顺滑。
这里顺便分享一个实用调试技巧:所有 DOM 节点均挂载 ht 属性用于标识身份,例如按钮 View 节点会标注 ht-view ht.ui.Button。
若遇到界面异常(如按钮不显示、样式错乱),可通过两步快速定位问题:先检查 DOM 壳子是否存在,存在则说明布局正常,问题出在绘制逻辑;不存在则是 DOM 布局层级出现异常,无需盲目排查海量标签。
04 性能这件事,打个不一样的比方
只说“性能更快”太过笼统,等于是在耍流氓。下面我们用通俗的类比,让大家直观理解性能差异的核心原理。
想象一块白板,上面要呈现一千个图形。
DOM 方案,如同往白板上粘贴独立磁贴。每个图形都是一块独立磁贴,可单独拖拽、叠放、改样式,灵活性高,但弊端极其明显:白板需要逐个记录每一块磁贴的位置、层级、边界信息。磁贴数量越多,白板的承载压力越大,挪动单块磁贴,还可能引发周边大面积磁贴错位、重排。
纯 Canvas 方案,则是直接用画笔在白板上绘制图案。一千个图形,终究只是白板上的一片笔迹;需要修改局部内容时,仅擦除对应区域、局部重绘即可。无论绘制十个还是一千个图形,白板始终是唯一的载体,无需逐个记录元素信息,仅需管控“当前帧需要刷新的区域”。
落地到实际开发中:HT UI 重绘时,仅会刷新产生变化的“脏区域”,通过一次 requestAnimationFrame 完成所有绘制更新,无需遍历、处理成千上万个 DOM 节点。
节点数量恒定、绘制刷新范围可控,这就是 HT UI 在重场景下依旧稳帧流畅的底气。
当然,客观来说,无需神化这套能力:如果只是开发仅有几张表单的常规后台管理系统,HT UI 的高性能优势完全无法发挥,实属大材小用。但只要你的界面存在“元素密集、状态多变、交互频繁”的特点,这套 Canvas 绘制架构的优势就会彻底凸显。
05 上手体验:极简代码快速落地
理论铺垫再多,不如实战验证。下面用最简示例,直观感受 HT UI 的上手门槛。首先引入两个文件:
<script src="ht.js"></script>
<scriptsrc="ht-ui.js"></script>
随后,仅需 5 行代码,即可实现一个可点击的功能按钮:
// 创建按钮实例
var button = new ht.ui.Button();
button.setText('Hello World');
// 绑定点击监听事件
button.on('click', function (e) {
alert('Hello World');
});
// 挂载至页面:左上角定位,宽高自适应内容
button.addToDOM(window,{ x: 10, y: 10, width: 'wrap_content', height: 'wrap_content' });
注意代码中的 wrap_content 机制,熟悉 Android 开发的开发者会十分熟悉,即“包裹内容自适应尺寸”,按钮宽高由内部文字、内容自动适配。HT UI 刻意沿用业界成熟的命名规范与设计范式,降低学习成本,首次使用也能凭直觉上手。
06 两块基石:Border 与 Drawable
想要自定义按钮样式、实现个性化皮肤,需要先了解 HT UI 的两大核心基础能力:Border(边框)与 Drawable(可绘制对象)。前者统一管控组件边框样式,后者负责实现组件背景、图标等视觉内容。二者均为接口,内置丰富的默认实现,无法满足需求时,可直接继承拓展、自定义绘制逻辑。
先看背景样式配置。仅 1 行代码,即可将按钮背景设置为圆角纯色样式:
// ColorDrawable:纯色背景 + 自定义圆角半径
button.setBackground(new ht.ui.drawable.ColorDrawable('#1ABC9C', 6));
Drawable 体系支持丰富的绘制能力。其中 ImageDrawable 专门用于图片渲染,内置 fill、uniform、centerUniform、center、cover 五种图片拉伸模式(默认 centerUniform),一键即可实现图片铺满、等比缩放、居中展示等常见效果。
除此之外,NinePatchImageDrawable(九宫格拉伸)更是适配各类异形组件的利器。移动端开发者对此并不陌生:通过九宫格机制,可实现图片四角固定不变、中间区域自由拉伸的效果,气泡弹窗、按钮背景、圆角面板等组件,靠一张素材图即可适配所有尺寸,极大精简资源文件。HT UI 的九宫格格式与 Android 原生规范基本一致,支持通过官方 draw9patch.jar 工具快速制作适配素材。
// 九宫格背景:四角固定不变,中段任意拉伸无模糊失真
view.setBackgroundDrawable(new ht.ui.drawable.NinePatchImageDrawable('test.9.'));
边框能力同样具备高度开放性。内置边框样式可满足绝大多数常规场景;若需要实现异形、个性化边框,只需继承 ht.ui.border.Border,重写 getLeft/getRight/getTop/getBottom 方法定义边框宽度,并重写 drawBorder 方法即可自主绘制。绘制前需通过 g.beginPath() 开启独立路径,既能保障绘制性能,又能避免样式串色,这也是 Canvas 绘制的基础规范。
这种“内置能力全覆盖、拓展接口全开放、支持全权接管绘制逻辑”的设计,正是后续文章要拆解的“组件丰富度”与“样式百变适配能力”的核心根基。
一支笔好不好,不只看它自带多少颜料,更看你想调配独特色调时,它会不会把你束缚。
07 这一篇,请记住三句话
- HT UI 并非传统的 DOM 组件库,它将所有视觉渲染能力交由 Canvas 实现。极简 DOM 壳子承接交互事件,Canvas 画笔全权负责视觉呈现。
- 它为高密度、高复杂度界面场景而生。重型工具软件、可视化搭建平台、大数据表格与树形结构、多控件密集编辑器,控件越多、交互越密集,性能优势越突出。
- 它极致降低开发与上手成本。五行 Hello World,贴近直觉的命名规范、开放完备的拓展接口,性能强劲却极易上手。
下一篇,我们将聚焦”量”与”序”
- HT UI 究竟提供了多少组件?
- 底层那套万物归一的 DataModel——为什么在 HT 的体系里,一个按钮和一万个节点,本质上是同一种存在。
- ……


