欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

web在线页面编辑实现:abtest可视化实验讲解

程序员文章站 2022-03-22 12:54:46
前言 最近我们开发了a/b testing 平台,开发web可视化实验中,涉及到页面在线编辑的实现,本文对此展开叙述。我在另一篇文章里也简单做了分享,有兴趣的可以点击查看abte...

前言

最近我们开发了a/b testing 平台,开发web可视化实验中,涉及到页面在线编辑的实现,本文对此展开叙述。我在另一篇文章里也简单做了分享,有兴趣的可以点击查看abtest-可视化实验sdk编辑功能开发浅谈。

功能介绍

用户接入我们a/b testing平台,可选择三种实验类型,可视化实验是其中一种便于用户快速上手的实验类型。简单来说,用户创建好可视化实验后,在目标页内针对元素属性进行实时编辑。实验开始后,sdk将拉取该配置进行渲染操作。

这里核心功能实现元素的编辑操作,有以下步骤:

1. 进入目标页后,获取当前实验版本的元素配置信息,同时将这些已配置的元素原版本信息保存起来,然后再根据配置信息渲染元素;

2. 加载编辑模块,分为:属性编辑模块和元素选择模块;

3. hover、select某个元素后,在该元素上绘制蒙层做标记;

4. select某个元素后,采集该元素属性信息,推送给属性操作模块;

5. 属性操作模块显示推送过来的属性值,每次编辑属性时,推送给元素选择模块进行实时渲染;

6. 属性编辑模块支持操作记录回退、前进、保存操作;

开发实现

接下来我们按照上个介绍的步骤,讲解下实际开发要点。

1. 编辑模式

当页面打开时,我们会在页面url上添加hubble_abtest_visual_key标志,然后sdk根据该标志确定进入编辑模式,依赖该标志拉取实验配置信息,以及加载编辑模块,

拉取的实验配置托管给编辑模块渲染。

由于我们平台当前不支持a/a 测试,所以若没有编辑,我们是不允许保存操作的。要实现这个判断,我们需要将元素配置前的信息和配置后信息做对比。故在配置渲染前,我们要保存下当前的元素信息。

2. 属性编辑模块

属性编辑模块,包含记录操作和元素属性编辑操作两大功能,这里就讲解下属性编辑功能实现要点。

当前我们支持编辑元素属性有:尺寸、文本、背景、边框、提示信息、目标链接。

配置信息数据结构

一个元素将配置如下信息:

{
    selector: "#analytics > a"
    css: {
        "background-image":"none",
        "border-color":"rgba(0, 0, 0, 0.65)",
        "border-style":"none",
        "border-width":"0px",
        "color":"rgba(0, 0, 0, 0.65)",
        "display":"block",
        "font-size":"14px",
        "font-weight":"400",
        "height":"42px",
        "text-align":"start",
        "visibility":"visible",
        "width":"1920px"
    },
    attributes: {
        placeholder: "",
        "href": "javascript:;"
    },
    nodename: 'a'
}

selector 表示元素的选择器;css 表示元素的样式信息;attributes 表示元素的属性信息;nodename 表示元素的标签类型;

尺寸

尺寸:元素的 width、height、显示、隐藏、删除。

其中 width 和 height 需要用户自己填入,显示操作是tab按钮选择,这些都属于css类设置。

比如 width 100px ,height 100px ,元素隐藏(占坑):

{
    css: {
        width: '100px',
        height: '100px',
        display: '';
        visibility: 'hidden'
    }
}

文本

文本: 元素的 color、font-size、font-weight、text-align。

文本的color,我们引入了一个颜色选择器,方便用户操作。

配置信息如下:

{
    css: {
        color: 'rgba(0, 0, 0, 0)',
        "font-size":"14px",
        "font-weight":"400",
        "text-align":"center"
    }
}

背景

背景: 元素的 background-image、background-color。

背景图片我们允许用户填入一个图片地址或者上传一张图片。

配置信息如下:

{
    css: {
        "background-color":"rgb(221, 221, 221)",
        "background-image":"none"
    }
}

边框

边框:元素的 border-color、border-style、border-width。

配置信息如下:

{
    css: {
        "border-color":"rgba(0, 0, 0, 0.65)",
        "border-style":"none",
        "border-width":"0px"
    }
}

提示信息

提示信息,针对 input、textarea元素的 placeholder 属性。

配置信息如下:

{
    attributes: {
        placeholder: ""
    }
}

目标链接

目标链接,针对 a元素的 href 属性。

配置信息如下:

{
    attributes: {
        href: "###"
    }
}

上面我们介绍了编辑一个元素涉及的属性信息,这块确定后,我们接下来讲解下元素选择模块。

3. 元素选择模块

该模块包含选择元素和渲染属性两功能,下面主要讲解下元素选择功能。

一个页面,上面有各种可交互的元素,当我们在编辑时,应当禁止这些交互。实现这些方式有很多种,这里我们通过在body最底部覆盖一层蒙版实现。

// js
// 在body底部插入一个蒙层元素
const hubbleoverlay = document.createelement('hubbleoverlay');
hubbleoverlay.classname = 'hubble-abtest-page-overlay';
document.body.append(hubbleoverlay);
/** css **/
.hubble-abtest-page-overlay {background: transparent;display: block;position: fixed;right: 0px;top: 0px;width: 100%;height: 100%;margin: 0 !important;padding: 0 !important;z-index: 2147483647 !important;}

上面的实现,将带来一个问题,无法正确选择页面元素了,下面我们都将这个问题为前提实现元素选择功能。

正确获取选择的元素

要获取元素,首先我们在body上绑定一个监听事件,同时禁止右键,避免干扰。

// js
const handleevent = function(e) {
    e.preventdefault();
    e.stoppropagation();
    if (e.type === "contextmenu") {
        return false;
    }
    if (e.type === 'click') {
        handleclick(e);
    }
};
document.body.addeventlistener("click", handleevent);
document.body.addeventlistener("contextmenu", handleevent);

当点击元素时,触发了 handleclick 方法,此时我们获取到的 e.target 是 hubbleoverlay,这并非我们想要的。

这里我们将使用 document.elementfrompoint 方法,传入 e.clientx, e.clienty两个参数后,我们将获取当前文档上处于指定坐标位置最顶层的元素(点击了解该api)。

我们在 handleclick 方法内,首先将 hubbleoverlay蒙层宽度设置为0,然后调用 document.elementfrompoint方法,获取想要的元素,最后还原hubbleoverlay蒙层宽度。

// js
const getelementfrompoint = function(e) {
    const $overlay =  document.getelementsbyclassname('hubble-abtest-page-overlay')[0];
    $overlay.style.width = '0';
    const $element = document.elementfrompoint(e.clientx, e.clienty);
    $overlay.style.width = '';
    return $element;
};
const handleclick = function(e) {
    const $element = this.getelementfrompoint(e);
};

元素上绘制选中状态

点击元素后,我们已经正确获取到元素对象,此时需要在元素上绘制一个蒙层,表示已选择了该元素。由于页面上已经被hubbleoverlay蒙层覆盖,我们绘制的元素蒙层层级需要比hubbleoverlay蒙层高,故我们在body底部再次新增一个元素,作为该元素的蒙层。

// js
const types = {
    'hover': 'hubble-abtest-hover',
    'selected': 'hubble-abtest-selected'
};
const highlight = function(e) {
    const selector = e.selector || '';
    const $elarr = _.queryselectorall(selector);
    for(let i = 0; i < $elarr.length; i += 1) {
      let $p = document.createelement('p');
      $p.classname = 'hubble-abtest-cursor ' + types[e.type];
      document.body.insertbefore($p, _.queryselector('.hubble-abtest-page-overlay'));
      //设置元素蒙层的样式,width、height、left、top
      _.setoverlaypropertiesforelement($elarr[i], $p);
    }
};
const handleclick = function(e) {
    const $element = this.getelementfrompoint(e);
    const selector;
    highlight({
       selector: selector,
       type: 'selected'
    });
};
/**css**/
'.hubble-abtest-cursor {position: fixed; background-color: rgba(0, 107, 255, 0.21); border: 1px solid rgba(0, 107, 255, 1); z-index: 2147483647 !important;pointer-events: none;border-radius: 2px;box-sizing: content-box;margin: 0 !important;padding: 0 !important; }'

设置元素蒙层样式

上面我们已经在页面上添加了选中元素的蒙层p了,但是由于setoverlaypropertiesforelement未实现,该p并不能正确定位到元素上,接下来我们讲解下该方法的实现。

setoverlaypropertiesforelement(originel, targetel)方法,对应的参数说明下,originel 表示选中的元素对象,targetel表示元素蒙层对象,其实就是我们通过获取选中元素的属性,来确定元素蒙层的位置。

要确定元素蒙层的位置,我们需要确定该蒙层的 width、height、left、top这四个属性。

首先我们要获取选中元素的 top、left、width、height。

获取选中元素的 top、left 方法 offset:

const _ = {};
_.offset = function(itemel) {
    if (!itemel) return;
    const rect = itemel.getboundingclientrect();
    if ( rect.width || rect.height ) {
      const doc = itemel.ownerdocument;
      const docelem = doc.documentelement;
      // 兼容ie写法 ==》  - docelem.clienttop、 - docelem.clientleft ,其它浏览器为0px,ie为2px
      return {
        top: rect.top + window.pageyoffset - docelem.clienttop,
        left: rect.left + window.pagexoffset - docelem.clientleft
      };
    }else{
      return {
        top: 0,
        left: 0
      }
    }
};

获取选中元素的 width、heigt 方法 getsize:

_.getsize = function(itemel) {
    if (!itemel) return;
    if (!window.getcomputedstyle) {
      return {width: itemel.offsetwidth, height: itemel.offsetheight};
    }
    try {
      const bounds = itemel.getboundingclientrect();
      return {width: bounds.width, height: bounds.height};
    } catch (e){
      return {width: 0, height: 0};
    }
};

获取页面left top方向间距 getelementspacingoffset:

_.getelementspacingoffset = function(direction) {
    const $html = document.getelementsbytagname('html')[0];
    const $body = document.getelementsbytagname('body')[0];
    const scroll = (direction === 'top' ? window.scrolly : window.scrollx);
    const htmlpadding = parseint(this.getstyle($html, 'padding-' + direction));
    const htmlmargin = parseint(this.getstyle($html, 'margin-' + direction));
    const htmlborder = parseint(this.getstyle($html, 'border-' + direction));
    const bodyborder = parseint(this.getstyle($body, 'border-' + direction));
    let a = 0;
    if (htmlborder > 0 && bodyborder > 0) {
      a = htmlborder + bodyborder;
    }
    return parseint(htmlpadding + htmlmargin + scroll + a, 10);
};

然后选中设置蒙层的样式实现 setoverlaypropertiesforelement:

_.setoverlaypropertiesforelement = function(originel, targetel) {
    const offset = this.offset(originel);
    const getsize = this.getsize(originel);
    const elementbounds = {
      bottom: offset.top + getsize.height,
      top: offset.top,
      left: offset.left,
      right: offset.left + getsize.width,
      width: getsize.width,
      height: getsize.height
    };
    const setoverlaypropertiesforelementleft = this.getelementspacingoffset('left') + 1;
    const setoverlaypropertiesforelementtop = this.getelementspacingoffset('top') + 1;
    targetel.style.top = (elementbounds.top - setoverlaypropertiesforelementtop)  + 'px';
    targetel.style.left = (elementbounds.left - setoverlaypropertiesforelementleft)  + 'px';
    targetel.style.width = elementbounds.width + 'px';
    targetel.style.height = elementbounds.height + 'px';
};

这里要说明下:若选中元素单位为em,rem这些,可能存在问题,本文到此并未实际测试。

设置元素蒙层样式是个非常重要的实现,故本文贴出详细代码供大家参考。

获取选中元素信息

上面我们已经获取到选中的元素,同时也加了选中标志。之前已讲解过,我们还需提取选中元素的属性信息,这些属性信息请参考编辑模块那节。

首先我们继续在 handleclick方法内,触发信息trigger。

// js
const handleclick = function(e) {
    //省略....
    //获取元素选择器
    const $element = getelementfrompoint(e);
    const selector = _.getdomselector($element);
    //省略....
    // 获取元素信息
    const elementinfo = _.getelementinfo($element);
    _.trigger('selected',{
        selector: selector,
        css: elementinfo.css,
        attributes: elementinfo.attributes,
        nodename: elementinfo.nodename
    });
};

接下来讲解下 getelementinfo 方法的实现。

为了获取选中元素的css样式,我们实现了 getstyle方法,我们知道,用document.getelementbyid('element').style.xxx 可以获取元素的样式信息,可是它获取的只是dom元素style属性里的样式规则,对于通过class属性引用的外部样式表,就拿不到我们要的信息了。所以我们实现如下:

_.getstyle = function(itemel, csskey) {
    // 兼容ie
    if(itemel.currentstyle){
      return itemel.currentstyle[csskey];
    }else{
      return itemel.ownerdocument.defaultview.getcomputedstyle(itemel, null).getpropertyvalue(csskey);
    }
};

解决了获取元素css样式问题,getelementinfo 方法就好实现了:

_.getelementinfo = function(itemel) {
    if (!itemel) {
      return null;
    }
    const obj = {
      nodename: itemel.nodename,
      html: itemel.innerhtml,
      outerhtml: itemel.outerhtml,
      css: {
        'width': _.getstyle(itemel, 'width'),
        //省略...
        'border-width': _.getstyle(itemel, 'border-width')
      },
      attributes: {
      }
    };
    if (itemel.nodename === 'a') {
      obj.attributes.href = itemel.getattribute('href');
    }
    if (itemel.nodename === 'input' || itemel.nodename === 'textarea') {
      obj.attributes.placeholder = itemel.getattribute('placeholder');
    }
    return obj;
};

页面滚动和缩放

当我们页面滚动和缩放时,所绘制的元素蒙版会出现定位错乱情况,此时解决方式就是调用setoverlaypropertiesforelement 方法重新设置样式。但该方法需要拿到当前选中的元素,故每次选中元素时,我们就保存该元素,这里要特别注明:整个操作中,当前只能有一个被选中的元素。若可选择多个,考虑的可能是蒙层和元素之间的一一对应关系了,本文无需考虑。

// js
const selectedelement = null;
const rerender = function() {
    const $hubbleabtestcursorselected = _.queryselector('p.hubble-abtest-selected');
    if ($hubbleabtestcursorselected) {
      _.setoverlaypropertiesforelement(this.selectedelement, $hubbleabtestcursorselected);
    }
};
window.addeventlistener('resize', () => {
  settimeout(() => rerender(), 50);
});
window.addeventlistener('scroll', () => {
  settimeout(() => rerender(), 50);
});

上面我们延迟了50ms重设,只是简单确保页面变动已完毕。

鼠标hover

当然我们每次鼠标hover一个元素时,也会绘制元素蒙层,该实现方式跟选中元素基本一致,本文不再讲解了。