授权转载自:石头人汉考克,https://juejin.cn/post/6844904083120193543
前言
公司经常为了活动推广营销,拉新留存,制作临时活动页面,且组件大体相似,为了提高运营的工作效率,减少开发成本,基于此开发一个活动可视化搭建项目,让运营可以通过,点击和拖拽组件,选择或导入数据的的方式,快速生成活动页面上线,在此做一个小小的总结。
核心设计
大体流程是:
创建 -> 编辑 -> 保存 -> 发布 -> 展示
核心:
维护一个obj,保存着个组件的父子关系,像一个node树
,每个组件都有唯一的ID,举例如下
const nodeTree = {
id: 'component0',
name: 'rootContainer',
children: [
{
id: 'component1',
name: 'header'
},
{
id: 'component2',
name: 'content',
children: [],
}
]
}
创建一个obj,编辑时 不操作dom,就是增删改查obj数组,来更新视图
保存时obj存在数据库,在服务器某个地址生成html文件,静态资源, obj通过模版传递挂载在window上,并生成唯一访问路径
发布时改变当前活动页面可访问状态
展示时,根据obj渲染指定的定制组件生成页面
重点
1.节点操作
不操作dom节点,通过对数组对象的增删改查来更新视图
2.拖拽与判定
编辑时,涉及到拖拽,判断点与矩形相交,设置偏移量,来区分同级插入,或子级插入,以及提示信息
拖拽:也不是完全利用HTML5 拖放(Drag 和 drop)事件,而是用其监听用户操作,在
dragStart
(拖动开始),dragOver
(拖动到可释放区),dragEnd
(拖动结束) ,drop
(放下)时进行相应的数据传递和增删改查的动作判断点与矩形相交:当拖拽一个组件悬停到可放置组件区域,用户可能是想放在悬停组件的上面,下面,左面,右面,里面五种可能(块级元素为上下里,行级元素为左右里)我们需要为多种选择划分相应的区域,和明确的提示
重点逻辑
export const relativePositionJudge = (cur, box, isDrop, direction) => {
const rect = box.getBoundingClientRect();
let offset = null;
if(direction) {
offset = {
x: rect.width / 2,
y: 0,
};
} else {
offset = {
x: 0,
y: isDrop ? rect.height / 4 : rect.height / 2,
};
}
const point = {
x: cur.clientX,
y: cur.clientY,
};
// up
const rect1 = {
x: rect.x + offset.x,
y: rect.y,
w: rect.width - (offset.x * 2),
h: offset.y,
};
// down
const rect2 = {
x: rect.x + offset.x,
y: (rect.y + rect.height) - offset.y,
w: rect.width - (offset.x * 2),
h: offset.y,
};
// inside
const rect3 = {
x: rect.x + offset.x,
y: rect.y + offset.y,
w: rect.width - (offset.x * 2),
h: rect.height - (offset.y * 2),
};
// front
const rect4 = {
x: rect.x,
y: rect.y,
w: offset.x,
h: rect.height,
};
// behind
const rect5 = {
x: rect.x + rect.width - offset.x,
y: rect.y,
w: offset.x,
h: rect.height,
};
let pos = null;
if (pointInRect(point, rect1)) pos = 'up';
if (pointInRect(point, rect2)) pos = 'down';
if (pointInRect(point, rect3)) pos = 'inside';
if (pointInRect(point, rect4)) pos = 'front';
if (pointInRect(point, rect5)) pos = 'behind';
return pos;
};
const pointInRect = (point, rect) => {
return point.x >= rect.x && point.y >= rect.y && point.x <= rect.x + rect.w && point.y <= rect.y + rect.h;
};
cur
为当前拖拽的组件,可通过其获取鼠标当前坐标box
为当前悬停区域,通过getBoundingClientRect
方法获取宽高及位置isDrop
为当前区域是否可放置,direction
为当前区域元素的排列方向,通过两者设置,横向或纵向的偏移量大小,假如当前区域纵向排列,且可放置,则把可放置区域由上至下分成3份,1/4,1/2,1/2(具体偏移量可按照需求或用户体验自由设置)
通过鼠标移动当前坐标与分份后区域四个角坐标比较,确定位置,进而做放置提示和节点插入
请看下图演示
3.组件与渲染
每一类定制组件都有唯一的name名,每一个组件在node树中被创建时也有唯一id,方便后期的编辑和渲染,
遍历node树递归调用主渲染文件,根据组件name名和相应数据,渲染出对应组件
4.移动端适配和预览
由于移动端和PC端样式和差异较大,就没考虑一套代码自适应,每个定制组件对应两个文件PC和h5,渲染展示时,判断当前平台进而作出相应的展示
h5预览使用iframe,h5预览单独占一个路由,赋值给iframe的src属性
5.文字快速编辑
活动页面上会涉及很多文字,用户想修改,有几种方法
编辑按钮,把它变成输入框,完成后,保存按钮,
在属性栏放输入框做关系映射,
以上两种可能都不太直观,也比较麻烦
就想到了使用contenteditable
属性,给标签加上后,可直接修改文字,可设置双击修改,延时保存,并设置防抖,大多数组件都会存在此需求,直接标签绑定事件比较麻烦,因此设置了全局绑定事件监听,控制注册和及时销毁
请看下图演示
特点
编辑回退和取消回退
每一次操作后,都存储一下node树,并放入回退队列,,通过指向队列的上一个或下一个来实现回退和取消回退,通过并限制队列长度,控制浏览器内存使用
组件上下移动和指向父组件功能
用户编辑时,可能会对组件的位置进行调整,还有组件嵌套层级关系过多时,可能选中当前组件的父组件比较困难,基于此提供了这两个功能,
具体实现,就是通过组件的唯一Id,遍历node树查找,删除当前组件,然后插入在兄弟节点的上面或下面,
思考和优化
关于活动页保存展现的心路历程:单独开一个项目,或项目单独开一个页面,作为活动展示使用,根据唯一id,获取不同数据渲染配置页面
问题:
代码不存粹,代码量较大,包含了所有定制组件模版
项目出现问题影响所有页面
项目或组件出现改动,要考虑对在线活动的影响
所以此想法被PASS,每创建保存一个活动页,都会在服务器固化的生成唯一的html文件和静态资源,保证不被影响
优化想法:直接把编辑好的活动页面html片段传给后端,后端直接生成渲染好的活动页面,
优点:
访问页面时不用再根据node树临时渲染,页面加载效率提高,
代码量减少
总结
总体是满足了产品需求,同时从三方面考虑
提高运营人员搭建页面工作效率, 增强产品可用性
降低开发人员编写定制组件难度和上手难度, 提高项目可维护性与可拓展性
优化用户体验,增强页面加载效率,(其他方面比如:可读性,可观赏性,可操作性)
点个『在看』支持下