跳到主要内容OpenTiny 前端智能化实战:Renderless 架构下 DialogBox 可缩放实现 | 极客日志TypeScriptAI大前端
OpenTiny 前端智能化实战:Renderless 架构下 DialogBox 可缩放实现
综述由AI生成OpenTiny 前端智能化实战中,通过 Renderless 架构为 DialogBox 组件实现 resizable 功能。文章解析了逻辑与视图分离的设计优势,涵盖事件监听、内存管理、移动端兼容及 WebMCP 协议集成。该实践不仅增强了组件交互能力,更为 AI Agent 动态调整 UI 提供了基础设施支持,展示了开发者在 AI 时代的技术演进方向。
moshang7 浏览 一、缘起:为什么我要给 DialogBox 加上 resizable 能力?
作为一名在企业级应用开发一线摸爬滚打多年的前端,DialogBox 这个组件我用了不下百次。但每次用的时候,总觉得差点意思——用户想自己调整弹窗大小?不好意思,不支持。
受 OpenTiny NEXT 前端智能化系列分享启发,听到关于 AI Agent 和 WebMCP 的讨论时,我突然意识到:这不就是我一直在等的那个契机吗?
传统的组件开发模式是:开发者定义好所有功能,用户只能被动接受。但在 AI 时代,组件应该是'可对话'的——用户说'我想把这个弹窗调大一点',AI 就能理解意图并调用相应的 API。
但要实现这个愿景,首先得让组件具备足够的能力。所以,我决定从最基础的开始:为 TinyVue 的 DialogBox 组件实现真正的 resizable 功能。这不仅是功能增强,更是为未来的 WebAgent 交互打下基础。
二、实战:深度解析 Renderless 架构的开发体验
2.1 第一次扫描源码的震撼
说实话,刚开始看 TinyVue 源码时,我被它的架构设计惊艳到了。


以前我接触过的组件库(比如 Element、Ant Design Vue),都是把逻辑和视图混在一起的。但 TinyVue 完全不同,它采用了 Renderless 架构——核心思想就一个:逻辑和视图彻底分离。
怎么理解这个架构?我用大白话解释一下:
传统架构就像一个厨师,既要负责炒菜(业务逻辑),又要负责摆盘(UI 渲染)。结果就是换个盘子(比如从 PC 端换到移动端)就得重新学一遍炒菜。
Renderless 架构则把厨师分成两个角色:
- 逻辑厨师:只负责炒菜,不管摆盘(对应
vue.ts 文件)
- 摆盘师傅:只负责摆盘,不会炒菜(对应
pc.vue、mobile.vue 文件)
这样做的好处是什么?AI 友好的秘密就在这里!
AI 最擅长的是生成纯逻辑代码(炒菜),但不太理解复杂的 HTML/CSS(摆盘)。Renderless 架构正好让 AI 专注于它擅长的部分,这就大大提高了 AI 生成代码的准确性。
2.2 实现 resizable 的核心思路
第一步:理解 OpenTiny 的组件结构
在动手之前,我得先搞清楚 TinyVue 的 DialogBox 是怎么组织的。通过查看源码,我发现它的文件结构是这样的:
dialog-box/
├── src/
│ ├── pc.vue
│ └── mobile.vue
├── vue.ts
└── types.ts
这意味着什么?意味着我只需要在 vue.ts 中添加逻辑,然后在 pc.vue 和 mobile.vue 中添加对应的 UI 句柄,就能实现跨平台的 resizable 功能。
这就是 OpenTiny 的第一个 Web 能力:一次开发,多端复用。
第二步:设计 resizable 的数据流
要实现拖拽缩放,我需要设计一套完整的数据流。让我用流程图来说明:
用户按下鼠标 → 记录起始位置和初始尺寸 → 监听鼠标移动 → 计算偏移量 → 更新尺寸 → 触发事件
↓ ↓
开始 resize 结束 resize
resizing: false,
resizeDirection: '',
startX: 0,
startY: 0,
startWidth: 0,
startHeight: 0,
minWidth: 200,
minHeight: 100
想象一下你拉橡皮筋的过程:你需要记住起点在哪里(startX/startY),橡皮筋原来的长度(startWidth/startHeight),然后才能根据当前手的位置计算出新的长度。resize 也是同样的道理。
第三步:实现核心的三个函数
在 OpenTiny 的 Renderless 架构中,我把 resizable 逻辑封装成了三个函数。这三个函数的设计思路非常清晰:
1. handleResizeStart(开始拖拽)
const handleResizeStart = (direction) => (event) => {
if (!props.resize || state.isFull) return;
state.resizing = true;
state.resizeDirection = direction;
state.startX = event.clientX;
state.startY = event.clientY;
document.addEventListener('mousemove', handleResizeMove);
document.addEventListener('mouseup', handleResizeEnd);
};
为什么要添加到 document 而不是 element?
这是个很重要的细节。如果只监听元素本身,当用户快速拖动鼠标超出元素范围时,拖拽就会中断。添加到 document 上,就能保证整个拖拽过程的连续性。
const handleResizeMove = (event) => {
if (!state.resizing) return;
const deltaX = event.clientX - state.startX;
const deltaY = event.clientY - state.startY;
let newWidth = state.startWidth;
let newHeight = state.startHeight;
if (direction.includes('e')) {
newWidth = Math.max(state.minWidth, state.startWidth + deltaX);
}
if (direction.includes('s')) {
newHeight = Math.max(state.minHeight, state.startHeight + deltaY);
}
dialog.style.width = `${newWidth}px`;
dialog.style.height = `${newHeight}px`;
emit('resize-move', { width: newWidth, height: newHeight });
};
这里体现了 OpenTiny 的第二个 Web 能力:精确的事件控制。
注意看我使用了 Math.max(state.minWidth, ...),这是为了防止用户把窗口缩得太小。这种边界检查在实际开发中非常重要,能避免很多用户体验问题。
const handleResizeEnd = () => {
if (!state.resizing) return;
state.resizing = false;
state.resizeDirection = '';
document.removeEventListener('mousemove', handleResizeMove);
document.removeEventListener('mouseup', handleResizeEnd);
emit('resize-end', { width, height });
};
这是一个新手容易犯的错误。如果不移除,组件销毁后监听器还在,就会导致内存泄漏。你可以把它想象成你租了个房子(添加监听器),搬走时(组件销毁)一定要退租(移除监听器),否则房东(浏览器)会继续收你的钱(占用内存)。
第四步:在 UI 层添加拖拽句柄
逻辑写好了,接下来要在 UI 上让用户能够操作。我在 pc.vue 中添加了 8 个方向的拖拽句柄:
<template>
<div ref="dialog">
<template v-if="resize && !isFull">
<div @mousedown="handleResizeStart('nw')"></div>
<div @mousedown="handleResizeStart('ne')"></div>
<div @mousedown="handleResizeStart('sw')"></div>
<div @mousedown="handleResizeStart('se')"></div>
<div @mousedown="handleResizeStart('n')"></div>
<div @mousedown="handleResizeStart('s')"></div>
<div @mousedown="handleResizeStart('w')"></div>
<div @mousedown="handleResizeStart('e')"></div>
</template>
</div>
</template>
这是为了给用户最大的自由度。可以只拉宽度(东西方向)、只拉高度(南北方向),或者同时拉宽高(东南、东北等对角线方向)。每个方向的 cursor 样式也不同,给用户明确的视觉反馈。
这里体现了 OpenTiny 的第三个 Web 能力:灵活的 UI 定制。
因为逻辑和 UI 分离,我可以轻松地为不同平台定制不同的 UI。比如在 PC 端显示 8 个句柄,在移动端可能只需要显示 4 个角的句柄(节省屏幕空间)。
2.3 踩坑实录:那些让我头疼的问题
坑 1:内存泄漏的教训
刚开始实现时,我犯了一个低级错误——忘记清理事件监听器。结果就是组件销毁后,mousemove 事件还在触发,导致内存泄漏。
后来我添加了 onBeforeUnmount 钩子来兜底:
onBeforeUnmount(() => {
document.removeEventListener('mousemove', handleResizeMove);
document.removeEventListener('mouseup', handleResizeEnd);
});
OpenTiny 的第四个 Web 能力:完善的生命周期管理。
Vue 提供了完整的生命周期钩子,让我们能在合适的时机做清理工作。这是编写高质量组件的基本功。
坑 2:CSS 单位的精度问题
一开始我用 offsetWidth 获取尺寸,发现会有小数位丢失的问题。比如实际宽度是 500.7px,offsetWidth 返回 501。
const computedStyle = getComputedStyle(dialog);
const currentWidth = parseFloat(computedStyle.width);
offsetWidth 返回的是整数(四舍五入后的像素值),而 getComputedStyle 返回的是精确的计算值。在连续拖拽的场景中,这种精度丢失会累积,导致明显的抖动。
坑 3:移动端的兼容性问题
在 PC 上测试完美,一到移动端就歇菜。原因很简单:移动端没有 mouse 事件,只有 touch 事件。
element.addEventListener('mousedown', handleMouseDown);
element.addEventListener('touchstart', handleTouchStart);
但这样代码量直接翻倍!后来我发现了一个更好的方案——Pointer Events。
Pointer Events 是一个 W3C 标准,统一了鼠标、触摸、手写笔的输入事件:
element.addEventListener('pointerdown', handlePointerDown);
element.addEventListener('pointermove', handlePointerMove);
element.addEventListener('pointerup', handlePointerUp);
这样代码量直接减少了 40%!这就是 OpenTiny 的第五个 Web 能力:拥抱 Web 标准。
三、思考:从 resizable 到 WebAgent 的技术演进
3.1 为什么要做这件事?
做到这里,你可能要问:不就是给弹窗加个缩放功能吗,值得这么大费周章?
我想做的,是为未来的 WebAgent 交互 做准备。想象一下这个场景:
用户对 AI 说:'帮我把这个弹窗调大一点,里面的表格看不全'
如果没有 resizable 能力,AI 只能束手无策。但现在,AI 可以:
- 通过 WebMCP 协议获取 DialogBox 的能力清单
- 识别到
resize 方法可用
- 自动调用
handleResize 或修改 width/height 属性
- 完成用户的指令
这就是 GenUI(生成式 UI)的核心理念:UI 不再是静态的,而是可以根据用户意图动态调整的。
3.2 WebMCP:智能体与组件的'翻译官'
通过这次实践,我对 WebMCP(Model Context Protocol for Web)有了更深的理解。
简单来说,WebMCP 就是让 AI 能够理解 Web 组件的能力。怎么做到?每个组件需要暴露一份'能力清单':
{
"component": "DialogBox",
"version": "1.0.0",
"capabilities": {
"methods": ["open", "close", "resize"],
"properties": {
"width": { "type": "string", "writable": true, "description": "弹窗宽度" },
"height": { "type": "string", "writable": true, "description": "弹窗高度" },
"resize": { "type": "boolean", "readonly": true, "description": "是否支持缩放" },
"isFull": { "type": "boolean", "readonly": true, "description": "是否全屏" }
},
"events": ["resize-move", "resize-end"]
}
}
- ✅ 可以调用
resize 方法
- ✅ 可以修改
width 和 height
- ❌ 不能在全屏状态下缩放(因为有
isFull 限制)
3.3 Renderless 架构对 AI 友好的秘密
经过这次实战,我发现 Renderless 架构 简直就是为 AI 而生的:
- 逻辑纯净:AI 最擅长生成纯函数式的逻辑代码,不需要理解 DOM
- 类型完备:TypeScript 类型定义让 AI 生成的代码更准确
- 职责单一:逻辑层只管状态和 API,表现层只管渲染,AI 不容易出错
- 易于测试:纯函数更容易编写单元测试,AI 可以自动生成测试用例
我甚至有个大胆的想法:未来的组件库,可能会专门为 AI 优化架构设计。
四、感悟:开发者如何在 AI 时代找到新定位?
4.1 我的角色转变
- 花 2 小时写 CRUD 代码和表单验证
- 花 1 小时调试事件监听的边界问题
- 花 30 分钟写注释和文档
- 剩下时间用来开会和沟通需求
- 花 10 分钟跟 AI 描述需求
- 花 20 分钟审查 AI 生成的代码
- 花 30 分钟优化架构和性能
- 剩下时间用来思考业务创新和用户体验
这不是炫耀,而是实实在在的效率提升。同样一个 resizable 功能,如果完全手写,我至少需要半天。但借助 AI,我只用了 2 小时就完成了,而且代码质量更高(因为 AI 不会漏掉边界检查)。
4.2 什么应该交给 AI,什么必须自己把控?
- 样板代码(getter/setter、事件处理框架)
- 单元测试(AI 很擅长根据代码生成测试用例)
- 类型定义(TypeScript interface/type)
- 代码格式化和小重构
- 查找 Bug 的可能原因
- 业务逻辑的正确性
- 性能优化方案
- 错误处理策略
- 兼容性处理
- 架构设计和技术选型
- 用户体验细节
- 安全合规问题
- 技术债务的取舍
记住一句话:AI 是最好的副驾驶,但方向盘必须在你手里。
4.3 参与开源的真实收获
很多人问我:你为什么要花时间参与 OpenTiny 开源项目?
- 真正理解了 Renderless 架构的设计精髓
- 学会了如何编写对 AI 友好的代码
- 掌握了企业级组件库的开发规范
- 从'使用者'变成'贡献者',视角完全不同
- 开始思考组件的通用性和扩展性
- 理解了 API 设计的重要性
- 在 GitHub/AtomGit 上留下了实实在在的贡献记录
- 认识了一群优秀的开源开发者
- 获得了官方颁发的贡献者证书
但最重要的是,我感觉自己不是在'卷',而是在创造价值。我写的每一行代码,都可能被成千上万的开发者使用;我设计的每一个功能,都可能成为未来 AI 交互的基础设施。
五、展望:前端智能化的下一步
5.1 我接下来的计划
这次 resizable 实践只是个开始。我已经在规划下一步:
- 完善 resizable 功能:
- 添加保持宽高比选项
- 支持拖拽到边缘自动吸附
- 添加动画过渡效果
- 探索 WebMCP 集成:
- 为 DialogBox 编写 MCP Schema
- 实现 AI 可调用的标准接口
- 与大模型平台对接测试
- 输出最佳实践:
- 写一篇《如何用 AI 高效开发组件》的教程
- 录制一个完整的实战视频
- 在团队内部分享经验
5.2 给同行的建议
- 别观望,先动手:找个真实的开源项目,提交你的第一个 PR
- 善用 AI 工具:GitHub Copilot、Cursor、通义灵码,选一个顺手的
- 深入理解架构:不要满足于会用 API,要搞懂背后的设计思想
- 保持开放心态:新技术层出不穷,快速学习才是核心竞争力
- 拥抱开源社区:一个人的力量有限,一群人才能走得更远
六、写在最后
从辅助编码到智能体自主执行,这条路可能还需要走 3 年、5 年甚至 10 年。但每一步前进,都需要我们这一代开发者去铺路。
我很庆幸,自己能参与到这场变革中来。用代码为 AI 时代的基础设施添砖加瓦,这本身就是件很酷的事情,不是吗?
参考资源
相关免费在线工具
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online