活动可视化搭建(拖拽生成页面)

前端技术江湖

共 2414字,需浏览 5分钟

 ·

2021-01-17 19:34

授权转载自:石头人汉考克,https://juejin.cn/post/6844904083120193543

前言

公司经常为了活动推广营销,拉新留存,制作临时活动页面,且组件大体相似,为了提高运营的工作效率,减少开发成本,基于此开发一个活动可视化搭建项目,让运营可以通过,点击和拖拽组件,选择或导入数据的的方式,快速生成活动页面上线,在此做一个小小的总结。

核心设计

大体流程是:

创建 -> 编辑 -> 保存 -> 发布 -> 展示

核心:

维护一个obj,保存着个组件的父子关系,像一个node树,每个组件都有唯一的ID,举例如下

const nodeTree = {
  id: 'component0',
  name: 'rootContainer',
  children: [
      {
          id: 'component1',
          name: 'header'
      },
      {
          id: 'component2',
          name: 'content',
          children: [],
      }
  ]
}

  1. 创建一个obj,编辑时 不操作dom,就是增删改查obj数组,来更新视图

  2. 保存时obj存在数据库,在服务器某个地址生成html文件,静态资源, obj通过模版传递挂载在window上,并生成唯一访问路径

  3. 发布时改变当前活动页面可访问状态

  4. 展示时,根据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;
};

  1. cur为当前拖拽的组件,可通过其获取鼠标当前坐标
  2. box为当前悬停区域,通过getBoundingClientRect方法获取宽高及位置
  3. isDrop为当前区域是否可放置,direction 为当前区域元素的排列方向,通过两者设置,横向或纵向的偏移量大小,
  4. 假如当前区域纵向排列,且可放置,则把可放置区域由上至下分成3份,1/4,1/2,1/2(具体偏移量可按照需求或用户体验自由设置)
  5. 通过鼠标移动当前坐标与分份后区域四个角坐标比较,确定位置,进而做放置提示和节点插入

请看下图演示

3.组件与渲染

每一类定制组件都有唯一的name名,每一个组件在node树中被创建时也有唯一id,方便后期的编辑和渲染,

遍历node树递归调用主渲染文件,根据组件name名和相应数据,渲染出对应组件

4.移动端适配和预览

  • 由于移动端和PC端样式和差异较大,就没考虑一套代码自适应,每个定制组件对应两个文件PC和h5,渲染展示时,判断当前平台进而作出相应的展示
  • h5预览使用iframe,h5预览单独占一个路由,赋值给iframe的src属性

5.文字快速编辑

活动页面上会涉及很多文字,用户想修改,有几种方法

  1. 编辑按钮,把它变成输入框,完成后,保存按钮,

  2. 在属性栏放输入框做关系映射,

以上两种可能都不太直观,也比较麻烦

就想到了使用contenteditable属性,给标签加上后,可直接修改文字,可设置双击修改,延时保存,并设置防抖,大多数组件都会存在此需求,直接标签绑定事件比较麻烦,因此设置了全局绑定事件监听,控制注册和及时销毁

请看下图演示

特点

编辑回退和取消回退

每一次操作后,都存储一下node树,并放入回退队列,,通过指向队列的上一个或下一个来实现回退和取消回退,通过并限制队列长度,控制浏览器内存使用

组件上下移动和指向父组件功能

  • 用户编辑时,可能会对组件的位置进行调整,还有组件嵌套层级关系过多时,可能选中当前组件的父组件比较困难,基于此提供了这两个功能,

  • 具体实现,就是通过组件的唯一Id,遍历node树查找,删除当前组件,然后插入在兄弟节点的上面或下面,

思考和优化

  1. 关于活动页保存展现的心路历程:单独开一个项目,或项目单独开一个页面,作为活动展示使用,根据唯一id,获取不同数据渲染配置页面

问题:

  • 代码不存粹,代码量较大,包含了所有定制组件模版

  • 项目出现问题影响所有页面

  • 项目或组件出现改动,要考虑对在线活动的影响

所以此想法被PASS,每创建保存一个活动页,都会在服务器固化的生成唯一的html文件和静态资源,保证不被影响

  1. 优化想法:直接把编辑好的活动页面html片段传给后端,后端直接生成渲染好的活动页面,

优点:

  • 访问页面时不用再根据node树临时渲染,页面加载效率提高,

  • 代码量减少

总结

总体是满足了产品需求,同时从三方面考虑

  1. 提高运营人员搭建页面工作效率, 增强产品可用性

  2. 降低开发人员编写定制组件难度和上手难度, 提高项目可维护性与可拓展性

  3. 优化用户体验,增强页面加载效率,(其他方面比如:可读性,可观赏性,可操作性)

点个『在看』支持下 

浏览 77
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报