前言
最近我们开发了A/B Testing 平台,开发web可视化实验中,涉及到页面在线编辑的实现,本文对此展开叙述。我在另一篇文章里也简单做了分享,有兴趣的可以点击查看。
功能介绍
用户接入我们A/B Testing平台,可选择三种实验类型,可视化实验是其中一种便于用户快速上手的实验类型。简单来说,用户创建好可视化实验后,在目标页内针对元素属性进行实时编辑。实验开始后,sdk将拉取该配置进行渲染操作。
这里核心功能实现元素的编辑操作,有以下步骤:- 进入目标页后,获取当前实验版本的元素配置信息,同时将这些已配置的元素原版本信息保存起来,然后再根据配置信息渲染元素;
- 加载编辑模块,分为:属性编辑模块和元素选择模块;
- hover、select某个元素后,在该元素上绘制蒙层做标记;
- select某个元素后,采集该元素属性信息,推送给属性操作模块;
- 属性操作模块显示推送过来的属性值,每次编辑属性时,推送给元素选择模块进行实时渲染;
- 属性编辑模块支持操作记录回退、前进、保存操作;
开发实现
接下来我们按照上个介绍的步骤,讲解下实际开发要点。
1. 编辑模式
当页面打开时,我们会在页面url上添加hubble_abtest_visual_key
标志,然后sdk根据该标志确定进入编辑模式,依赖该标志拉取实验配置信息,以及加载编辑模块, 拉取的实验配置托管给编辑模块渲染。
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上绑定一个监听事件,同时禁止右键,避免干扰。// jsconst 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两个参数后,我们将获取当前文档上处于指定坐标位置最顶层的元素()。 我们在 handleClick 方法内,首先将 hubbleoverlay
蒙层宽度设置为0,然后调用 document.elementFromPoint
方法,获取想要的元素,最后还原hubbleoverlay
蒙层宽度。
// jsconst 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底部再次新增一个元素,作为该元素的蒙层。 // jsconst 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 $div = document.createElement('div'); $div.className = 'hubble-abtest-cursor ' + types[e.type]; document.body.insertBefore($div, _.querySelector('.hubble-abtest-page-overlay')); //设置元素蒙层的样式,width、height、left、top _.setOverlayPropertiesForElement($elArr[i], $div); }};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; }'复制代码
设置元素蒙层样式
上面我们已经在页面上添加了选中元素的蒙层div了,但是由于setOverlayPropertiesForElement
未实现,该div并不能正确定位到元素上,接下来我们讲解下该方法的实现。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。 // jsconst 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
方法的实现。
getStyle
方法,我们知道,用document.getElementById('element').style.xxx
可以获取元素的样式信息,可是它获取的只是DOM元素style属性里的样式规则,对于通过class属性引用的外部样式表,就拿不到我们要的信息了。所以我们实现如下: _.getStyle = function(itemEl, cssKey) { // 若可以拿到 style上的属性,则取它 if(itemEl.style[value]) { return itemEl.style[value]; } // 兼容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
方法重新设置样式。但该方法需要拿到当前选中的元素,故每次选中元素时,我们就保存该元素,这里要特别注明:整个操作中,当前只能有一个被选中的元素。若可选择多个,考虑的可能是蒙层和元素之间的一一对应关系了,本文无需考虑。 // jsconst selectedElement = null;const rerender = function() { const $hubbleAbtestCursorSelected = _.querySelector('div.hubble-abtest-selected'); if ($hubbleAbtestCursorSelected) { _.setOverlayPropertiesForElement(this.selectedElement, $hubbleAbtestCursorSelected); }};window.addEventListener('resize', () => { setTimeout(() => rerender(), 50);});window.addEventListener('scroll', () => { setTimeout(() => rerender(), 50);});复制代码
上面我们延迟了50ms重设,只是简单确保页面变动已完毕。
鼠标hover
当然我们每次鼠标hover一个元素时,也会绘制元素蒙层,该实现方式跟选中元素基本一致,本文不再讲解了。结尾
本文简单介绍了A/B Testing平台的可视化实验一些功能,讲解了实现页面编辑功能的技术要点,希望对大家有帮助。
@作者:白云飘飘(534591395@qq.com)
@github: 欢迎关注我的微信公众号:
或者微信公众号搜索新梦想兔
,关注我哦。