内容简介:早在应用的最初期,就已经有开发者想通过可视化的方式来解决应用 UI 编辑的问题,例如很早期的 VisualBasic,更或者是 Android Studio 的 UI 编辑器等等。好像所有的开发者都有一个希望通过可视化编辑解决 UI 问题的一个梦。
早在应用的最初期,就已经有开发者想通过可视化的方式来解决应用 UI 编辑的问题,例如很早期的 VisualBasic,
更或者是 Android Studio 的 UI 编辑器等等。
好像所有的开发者都有一个希望通过可视化编辑解决 UI 问题的一个梦。
当然作为 Web 开发者也一样,可视化编辑对于 Web 开发者来说更加有意义。这是由于 Web 应用的特性所决定的。但同样带来的挑战也是由于这个特性,Web 应用多种多样,千变万化,并且可能随着不同的设计潮流来改变原来的实现。所以到目前为止并没有一个很通用的可视化编辑工具。 纵览业界有名的页面搭建平台(如 weebly.com、wix.com、websitebuilder.com 等等),它们都有不错的视觉交互实现,但是也仅仅能够处理特定类型的 Web 应用。
可视化能力抽象
正因为这种变化性,作为一名工程师要做的就是要找到其中不变的部分以及会经常变化的部分,并 抽象 不变的部分,以形成一套通用的能力。 对于可视化编辑场景,不变的地方是什么?对,是编辑,就是页面的编辑功能。那么变化的地方是什么?是组件,组件本身会不断变化。但对组件的编辑能力是不变的。
对,上层抽象用一句化来概括就是
可视化能力抽象就是对组件编辑能力的抽象
有了这个基本思路,我们再深入思考一下,如何抽象组件编辑能力,并将这种能力通过一个产品表现出来。
对于这类应用,可以将它看成一个 Word 文档,也就是一个类编辑器应用。这样可以给自己一个思考的方向。所以也就形成了以下这些特性的抽象。
应用上层设计
按照上图所示,如果说我们将应用的所有功能都抽象出来了,接下来就要想办法去完善功能的细节,并且需要很明确这些功能之间的关系,在自己的脑海内必须形成一张图。
首先明确,我们要设计这样一个应用,首先哪些必要的因素。作为一个前端复杂应用,一般都会有一些通用的设计模式。
这张图将复杂应用划分成 4 个部分,分别是 编辑器、配置文件(JSON Schema)、运行时框架、以及云端存储。 本质上来讲就是将页面的组件抽象成一个个配置文件(JSON Schema),并且可以编辑该配置文件。然后可以通过一些手段持久化该配置文件。最后获取该配置文件并通过渲染器变成我们想要的页面。 整体的思路大致是这个样子。
架构模式上,大致会分成这么几个层次。这个分层参考了很多此类型应用的架构模式。
场景层:最顶层的场景,属于可视化编辑器的上层。 编辑层:这个层次就是对 Schema 进行编辑的输出地。 引擎层:这个层次的职责在于说提供操作 Schema 的 API 接口。 协议层:也就是最底层的 Schema 协议,用来描述页面的 JSON 文件。 渲染层:这里的渲染层可以理解成一个组件体系的驱动程序。例如 React 渲染器、Vue 渲染器等等。
就是这样一套体系,整个应用就可以完美的 Run 起来了。
应用细节设计
整个应用细节还算比较多,但真正有价值的细节点只有几个。
细节点一,层次的划分
这里首先来讲讲 编排层 和 渲染层的划分,其实这一块很多应用都有不同实现思路。 这里解释一下为什么有这两层的划分,其实关键点在于说是否将编辑和渲染放在一个层次中处理。
如果放在一个层次里处理,有些场景、例如占位符、拖拽体验上来讲会更加直接,更加友好。 但是面对这种复杂的交互体验,将渲染和编辑放在一起太容易造成这一层特别重,扩展性和可维护性上大打折扣。其实很多市面上主流的类编辑器应用都做了分层,但是我当时并不知道为什么需要将这两层区分来看待,最终我也采用了分层的方案,思考这种方案的时候没有想太多,先按照主流方案来。
开放、封闭原则开放封闭原则(OCP,Open Closed Principle)的核心点是在于封装变化,降低耦合。 既然分层了,那么一定追寻这个原则,顺着这个思路想, 分层就意味着需要面对这样的问题,因为两个层次虽然在某些地方是隔离开来的,但是仍然有些地方是需要联系的,这样就需要想明白哪些是是封闭的,那些是开放的?
在这里设计的整体采用 Event Bus 机制来处理两个层次对外通讯。 渲染层内部提供 Preview、Snapshot、Resize 等功能,对外通过事件机制将消息广播出去,给订阅该消息的模块得以处理。 而遮罩层提供了 RenderLine、Proxy、Insertion 等功能,设定响应拖拽的各种生命周期钩子来处理。在遮罩层上,几乎承载了页面上的所有的拖拽交互,是一个典型的消费者。
渲染层
遮罩层
细节点二,遮罩层计算
对于遮罩层的计算来看,原理其实也很简单,就是将 渲染层 的 DOM 计算结果同步到 遮罩层,然后 遮罩层 用 DIV 的方式绘制出对应的外框。
nodes.forEach((node) => { const id = node.getAttribute(hookIdKey); const parent = parentComponent(node, hookIdKey); const pid = parent && parent.getAttribute(hookIdKey); const nxy = xy[id] || utilsMeasures.xy(node); const pxy = xy[pid] || utilsMeasures.xy(parent); const wh = utilsMeasures.wh(node); xy[id] = nxy; xy[pid] = pxy; ret[id] = { x: nxy.x, y: nxy.y, l: nxy.x - pxy.x, t: nxy.y - pxy.y, w: wh.w, h: wh.h, p: wh.p, b: wh.b, m: wh.m, }; }); 复制代码
这一段就是获取 DOM 位置以及大小属性的关键信息。
细节点三,遮罩层分区
整个遮罩层会分成 4 个区域,分别是 Cover、 Handles、Insertion、Proxy 四个区域, Cover 的作用是拖拽的激活热点, Handles 的作用是额外的一些操作项, Insertion 的作用是插入提示点, Proxy 的作用是拖拽组件的占位符。
其他的分区还好,这里着重讲一下 Insertion 这个。这里的 Insertion 是计算拖拽需要插入的节点,那么如何去判定插入的节点是上下左右呢? 这里大致分为两步,
第一步,首先对拖拽热点进行缩放。
// 计算插入位置的中心点 function getAroundMidpoints(...args) { const [edge, gap = {}] = args; gap.x = gap.x || Infinity; gap.y = gap.y || Infinity; const indentX = Math.min(2 * gap.x, edge.width / 5); const indentY = Math.min(2 * gap.y, edge.height / 5); return { entity: { lt: [edge.x, edge.y], lb: [edge.x, edge.y + edge.height], rt: [edge.x + edge.width, edge.y], rb: [edge.x + edge.width, edge.y + edge.height], }, shadow: { lt: [edge.x + indentX, edge.y + indentY], lb: [edge.x + indentX, (edge.y + edge.height) - indentY], rt: [(edge.x + edge.width) - indentX, edge.y + indentY], rb: [(edge.x + edge.width) - indentX, (edge.y + edge.height) - indentY], }, }; } 复制代码
这里缩放的作用就是在组件原有边框大小区域下,进行一定比例缩放,形成一个虚框,通过虚框与实框间的间距作为热点,来判断拖拽的方向。
第二步,判定某个点是否在梯形区域内。
function nearestMode(coord, edge, modes, gap) { const { entity, shadow } = getAroundMidpoints(edge, gap); let ret = ''; if (inside([coord.x, coord.y], [entity.lt, shadow.lt, shadow.lb, entity.lb])) { ret = 'l'; } else if (inside([coord.x, coord.y], [shadow.lb, shadow.rb, entity.rb, entity.lb])) { ret = 'b'; } else if (inside([coord.x, coord.y], [shadow.rt, entity.rt, entity.rb, shadow.rb])) { ret = 'r'; } else if (inside([coord.x, coord.y], [entity.lt, entity.rt, shadow.rt, shadow.lt])) { ret = 't'; } else if (inside([coord.x, coord.y], [shadow.lt, shadow.rt, shadow.rb, shadow.lb])) { ret = 'v'; } return modes.includes(ret) ? ret : false; } 复制代码
这里的 inside 是利用 point-in-polygon 来判断拖拽点在哪一个热区范围内。从而确认放置的方向。
总结
这篇文章讲述的是从一个产品的雏形到产品的诞生的整体过程。
- 产品的背景,为什么会有这样的产品诞生?
- 产品的功能,这个产品需要做哪些事情,对产品能力的抽象。
- 产品的上层设计,此类型应用的架构设计思路。
- 产品的技术细节,特定细节的实现方案。
其实整个应用的各个点都值得思考。可以想象到这样一个应用的诞生过程并不容易,需要结合很多产品特性来综合考虑。希望能通过这篇文章引导大家一起来思考产品本身,而不仅仅限定在技术细节上。
如果大家看了这么久,都没有体感,那么下面一张图就是我们这个产品思路的一个实现。
文章可随意转载,但请保留此原文链接, 非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 写一个 React H5 可视化编辑器
- 介绍一个小工具:网络策略可视化编辑器
- CKEditor 4.8 发布,可视化 HTML 编辑器
- CKEditor 4.7.2 发布,可视化 HTML 编辑器
- TinyMCE 4.7.6 发布,可视化 HTML 编辑器
- CKEditor 4.9.2 发布,可视化 HTML 编辑器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
随机密码生成器
多种字符组合密码
XML、JSON 在线转换
在线XML、JSON转换工具