万字长文——全自动可视化骨架屏生成工具流程全记录!

一、业务背景

目前的App端开发大多采用H5+原生相结合,这种开发方式可在保证加载速度和页面稳定性的基础上最大化的降低开发成本, 因此xxxApp应用了此种开发模式。

在App内所嵌套的H5页面相比于普通HTML页面,页面首屏渲染速度会更慢,因为对于App内嵌的H5页面,除本身的页面资源加载(HTML+CSS+JS等)还要进行webView的加载,也因此比起普通HTML页面,APP内嵌H5页面对于首屏渲染优化要求更高,所以前端更要尽可能的去进行首屏渲染优化,才能给用户一个流畅的使用感受。

从页面渲染角度分析一个HTML页面的加载过程大体如下:

在这里插入图片描述

由上图可见影响页面首屏加载的原因包括HTML、CSS、JS这三类文件的下载、解析和执行以及与页面渲染有关的AJAX请求。但HTML与CSS文件体积较小,对白屏产生的影响不是很大,真正产生较大影响的是JS文件的下载与执行和AJAX请求的快慢。

如今前端业界内以上述分析原因为立足点提出的优化方案有如下四种:

  1. 服务端渲染(SSR)
  2. 预渲染(PreRender)
  3. Loading+ 离线包
  4. 骨架屏(Skeleton)+ 离线包

1)服务端渲染SSR:实现流程大体如下图
在这里插入图片描述

在SSR模式下,html的获取依赖服务器端获取数据的速度,不确定因素很多,因此不一定能缩短白屏时间;此外该方式实现与维护都较为复杂,需要开发代码兼容后端的runtime,搭建SSR架构,并在一定程度上增加了服务器的压力。

2)预渲染PreRender:实现流程大体如下图
在这里插入图片描述

PreRender模式下HTML中的首屏内容为项目构建时的内容,所以此种模式并不适用于动态页面。

3)Loading模式+离线包

该方式实现原理为在JS文件的下载与执行及AJAX请求到首屏渲染数据前的这一段时间内,以Loading字样代替白屏,并通过离线包加快AJAX数据的获取。此种方式中以Loading代替白屏虽然实现简单、方便,但是所有页面千篇一律,并无美感可言。

4)骨架屏+离线包

该方式实现原理同3),但相比于3)中以Loading代替白屏,此方案是以与当前页面结构相似的骨架屏去代替,效果更为个性化,从而很好的优化用户的体验。

综上所述,决定1.2版本中加入骨架屏。

二、预期骨架屏实现效果

在决定利用骨架屏去优化白屏问题后,预期的理想化骨架屏方案应该达到以下效果:

1)骨架屏结构:

  • 骨架屏结构类似于页面结构。
  • 骨架屏结构尽量简单,降低HTML的请求大小。

2)骨架屏样式:

  • 骨架屏展示效果流畅。过渡自然。

3)生成骨架屏的方案:

  • 能够高效、快速生成满足上述两点的骨架屏。

三、业界骨架屏实现方案

决定引入骨架屏去优化项目后,对如今业界的生成骨架屏方案进行了调研,生成方案可以分为两类,其中一类即为传统手动生成骨架屏,另一类为自动生成骨架屏。

3.1 传统手动生成骨架屏:

①根据UI设计或页面结构,开发人员手动编写符合预期的骨架屏样式代码并加到业务代码中。

②根据UI设计从Sketch中导出SVG或图片,并加到业务代码中。

优点:实现简单,无需考虑过多的逻辑与配置;骨架屏结构样式可控,可以生成完全符合预期的骨架屏。

缺点:代码量激增,且面对视觉设计的改版以及需求的更迭,骨架屏的跟进修改非常不便,此外此种方式与业务代码耦合度过高。

是否适用于xxx的骨架屏生成?

不适合。因为H5页面较多,每个页面都手写过于麻烦,效率不高,且不好控制骨架屏的插入时机。

3.2 自动生成骨架屏:

业界绝大多数自动化生成骨架屏的方案原理:使用puppeteer打开页面,分析页面结构并深度遍历页面DOM,通过一定的规则来生成对应的骨架屏代码,并通过webpack插件将骨架屏代码注入到HTML中,业界比较典型方案有四种:

1)vue-skeleton-webpack-plugin(百度)

GitHub:https://github.com/lavas-project/vue-skeleton-webpack-plugin

原理:该插件只是一种将写好的骨架屏代码自动注入到项目中的工具,并不能通过此插件实现自动生成骨架屏并注入到项目中的效果。

是否适用于XXX的骨架屏生成?

不适合。因为此过程比起传统骨架屏生成、只是在注入方式上有所改良,骨架屏样式代码依旧需要开发人员自行去写,效率较低,不符合预期。

2)draw-page-structure(京东)

GitHub:添加链接描述

原理:

  • 在配置文件中输入待生成页面的 URL,通过无头浏览器打开相应的页面。
  • 自动遍历页面中的所有节点,生成相应的样式hHTML到配置文件中指定的路径。

优点:

插件使用简单,无需过多配置,生成的骨架屏代码体积较小,对外提供了接口可自定义某些节点样式。

缺点:

需要在配置文件中输入网址生成骨架屏,并且修改骨架屏节点样式的方式也是在配置文件中预先设置。无论是生成还是调试其灵活性较差。此外内部遍历节点的生成逻辑不够完善,生成的骨架屏样式往往与真实页面有着较大误差。

是否适用于骨架屏生成?

不适合。每次生成骨架屏时都要到配置文件中重写url地址,自动化生成的骨架屏结构与真实页面相差过大,需要通过在配置文件中大量去写节点处理代码,大幅度降低了开发效率。

3)page-skeleton-webpack-plugin (饿了么)

GitHub:添加链接描述

原理:

  1. 通过无头浏览器puppeteer打开要生成骨架屏的页面。
  2. 注入提取骨架屏的脚本。
  3. 对页面中元素进行删减或增添,对已有元素通过层叠样式进行覆盖,达到在不改变页面布局下,隐藏图片和文字,然后通过样式覆盖,使得其展示为灰色块。然后将修改后的
    HTML 和 CSS 样式提取出来。

优点: 可以启动 UI 界面专门调整骨架屏,自动生成无需手写,提高开发效率。

缺点: 生成的骨架屏节点基于页面本身的结构和 CSS,存在嵌套比较深的情况,体积不会太小,并且只支持 history 路由模式;此外此种方案无法控制生成骨架屏的时机,导致生成的骨架屏样式与实际页面出入较大,生成的样式不够美观。

是否适用于骨架屏生成?

不适合。从原理上看,该插件是依据路由的不同从而生成相应的骨架屏页面,而H5页面是无路由、多页面结构项目,故无法应用该插件。

4)awesome-skeleton(考拉)

GitHub:https://github.com/kaola-fed/awesome-skeleton

原理:该方案核心思路参考了page-skeleton-webpack-plugin,生成的结构大同小异,此外它也是通过url方式访问原网页。但此外该方案额外提供可通过标识节点生成骨架屏。
优点: 可通过标识节点生成骨架屏,所以比起page-skeleton-webpack-plugin更加灵活。

缺点: 需要输入网址才能根据网址生成骨架屏,依赖于puppeteer,生成的骨架屏代码体积较大,且样式不够美观。

是否适用于骨架屏生成?

不适合。仅限node端使用,和dps一样生成页面时都要更改配置文件中的url地址,降低开发效率;并且插件自身环境配置较为复杂,需要开发人员去阅读文档进行学习才能够灵活的使用该工具。

此外,在本章节初就以提及这三种可自动生成骨架屏的插件(除vue-skeleton-webpack-plugin),其生成骨架屏节点时都依赖于puppteer,而因为puppteer本身的特性,借助puppteer去生成骨架屏会有一些额外的弊端:一是puppteer有时会下载失败甚至无法正常运行;二是puppteer体积较大,利用它生成,需要将其集成到项目本身,对项目本身体积有较大影响。

在这里插入图片描述

四、H5页面骨架屏实现

因上述方案无法满足预期需求,故结合项目特点,重新搭建一个骨架屏生成工具,该工具预期需求如下:

1、骨架屏可以自动化生成,也可以通过标识节点生成。

2、骨架屏块颜色可自定义化,可通过不同颜色区分重点内容。

3、生成骨架屏后可直接在页面进行修改、删除节点、添加动画等美化操作,从而最终生成符合预期的骨架屏。

4、生成的骨架屏可以直接插入到打包模板中,无需再手动向项目中加入骨架屏。

5、开发者可以控制调用骨架屏的时机。

根据前文中业界方案调研与上述需求,初步确定以下构建方案:
在需要生成骨架屏的页面中以页面的body为起始节点开始遍历(不再依赖puppeteer,不再手动设置url指定生成页面)。对于结构简单的页面,遍历后可通过自动化构建骨架屏的方式将生成的骨架屏展现在页面中,并能够与原始页面并排展示以作对比,且可在控制台进行节点样式修改。对于结构复杂的页面,可通过标识节点生成骨架屏。无论哪种生成方式最终点击保存后,生成的骨架屏结构可直接通过webpack注入到项目中,并可以在项目中控制骨架屏的关闭时机。

方案实现流程图如下:

在这里插入图片描述

由上图可知自动与自定义只是在遍历节点时逻辑有所不同。

五、详解方案实现原理:

5.1、环境配置

首先在webpack配置中利用webpack-inject-plugin初始化加载js脚本,当识别当前环境为skeleton环境时,自动加载相应js文件:

config.plugin('webpack-inject-plugin').use(
  new InjectPlugin(function() {
    let inject = "import * as monitor from './src/utils/monitor.js';"
    if (process.env.SKELETON === 'skeleton') {
      inject += "import * as dpsinit from './src/utils/vs/vsinit.js';"
    }
    return inject
  })
)

在package.json中配置好命令:npm run serve:ske 运行

5.2、自动化生成骨架屏遍历逻辑:

①确定能够生成骨架屏的元素:图片(背景)、按钮、Canvas、文字、音频视频、表单项、自定义特征的块等区域

②确定需要排除的节点:为保证页面的整洁度,排除一些不该出现在骨架屏中的不可见的DOM节点:即display为none,visibility为hidden,opacity为0等不可见元素

③深度遍历页面节点:指定一个 rootNode 作为入口节点(即待生成骨架屏页面的的根节点),由此入口进行递归遍历和筛选,初步先排除不可见节点(即在遍历到需要hidden的节点,先跳过)。

④计算得出骨架屏节点的宽、高、定位:通过 Element.getBoundingClientRect() 获取原页面节点宽、高、距离视口距离的绝对值,并在绘制时除以设计稿宽高*100%,即可求得以百分比为单位的宽高以及top、left。

⑤调用定义好的drawBlockAPI生成相应区域的颜色块(生成过程与元素属于结构层级,种类无关),但对于文本类标签如p、h、span标签等,绘制时宽高去除padding,其余的标签不用。

⑦绘制好后调用showBlocksAPI将绘制好的骨架屏显示在页面中以供调试。

核心代码如下:

//绘制骨架屏节点
function drawBlock({width,height,top,left,zIndex = 999,background = 'linear-gradient(90deg, #f0f0f0 25%, #e3e3e3 37%, #f0f0f0 63%);',radius,subClas} = {}) {
const styles = ['height:' + height + '%']
if (!subClas) {
styles.push('top:' + top + '%', 'left:' + left + '%', 'width:' + width + '%')
}
if (classProps.zIndex !== zIndex) {
styles.push('z-index:' + zIndex)
}
if (classProps.background !== background) {
styles.push('background:' + background)
}
radius && radius != '0px' && styles.push('border-radius:' + radius)
blocks.push(`
"_${subClas ? ' __' : ''}" style="${styles.join(';')}">
`)
}
//展示骨架屏节点
function showBlocks() {
  if (blocks.length) {
    //获取生成的骨架屏的根节点
    const skeletonWrap = document.getElementById('skeleton-dev')  
    //上述生成的div都存于blocks中,这里将其转换为字符串
    const blocksHTML = blocks.join('')
    const div = document.createElement('div')
    div.innerHTML = blocksHTML
    skeletonWrap.appendChild(div)
    //确定骨架屏展示区域
    window.scrollTo(0, this.originStyle.scrollTop)
    document.body.style.overflow = this.originStyle.bodyOverflow
    return blocksHTML
  }
}

5.3、个性化生成骨架屏遍历逻辑:

1)确定能够生成骨架屏的元素:在自动的基础上,更加细分了元素类别,分为文本类以及普通类。

2)确定需要排除的节点:同自动化原理。

3)生成骨架屏:大体和自动化差不多,区别是绘制前先判断节点是否拥有自定义属性,只有标签拥有needskeleton属性,才可以对应生成相应的骨架屏,并且当拥有deep属性时绘制出来的颜色块选用更深的颜色。

5.4、保存骨架屏到本地原理:

1)获取浏览器中的节点:点击save会像服务器的dps/save接口发起请求,并在前台获取页面中的div标签,并且将class类名为_和__的节点push到空数组中,最后转换成字符串,然后将字符串发送给后台。

2)确定保存字符串的路径:由于app端为文件夹名与页面的二级域名一致,所以 为了方便将字符串保存到对应的路径下,通过获取页面节点利用.baseURI以及进行过滤,得到相应的domain后将其返回到后台,进行路径拼接即可获得需要保存到的路径。

3)服务器端配置:服务器端需要完成处理前端返回的数据,并将其保存到本地,为了方便日后项目运行,即可自动加载打包,所以将字符串保存到每个路径对应的文件夹下的page.config.js中的skeleton属性下去,然后在模板中的相应位置加载这段字符串,即可实现打包时自动加载骨架屏。

4)结合webpack注入骨架屏,并控制展示时机:为了尽可能将白屏改变成骨架屏显示,缩短白屏时间,封装一个函数通过此函数控制骨架屏结束展示的时间,所以需要在每个页面中的渲染数据请求完毕处,调用该函数,让骨架屏隐藏,项目本身页面展现出来。

核心代码如下:

//获取domain的相关代码如下:
export default function getDomain() {
  const app = document.getElementById('appwrap-dev')
  let getUrlNode = { root: app }
  let domain = []
  const urlArr = getUrlNode.root.baseURI.split('')
  let index = 0
  for (let i = 0; i < urlArr.length; i++) {
    if (urlArr[i] === '/') {
      index++
    }
    if (index === 3) {
      domain.push(urlArr[i])
    }
  }
  domain.shift()
  let urlDomain = domain.join('')
  return urlDomain
}
//在打包模板中即可依赖htmlwebpackPlugin实现相应的控制:
<!-- 通过hasSkeleton属性判断当前页面是否需要骨架屏 -->
<% if(htmlWebpackPlugin.options.hasSkeleton) { %>
  <div style="display: none;" id='appWrap'>
        <div id="app">
        </div>
  </div>
    <div id="skeleton">
<!-- 骨架屏通过htmlWebpackPlugin在启动打包的时候自动注入 -->
    <%= htmlWebpackPlugin.options.loading %>
    </div>
 <!-- 不需要骨架屏直接展示原页面 --><% } 
else{%>
 <div id='appWrap'>
    <div id="app">
  </div>
 </div>
  <% } %>

5.5、控制骨架屏隐藏原理:

在需要骨架屏时先将项目页面设置为display: none; 然后通过封装一个隐藏骨架屏显示原项目的函数,去实现控制骨架屏的消失时机,相关代码如下:

const hideSkeleton = () => {
  const app = document.getElementById('appWrap')
  const skeletonPage = document.getElementById('skeleton')
  app.style.display = 'block'
  skeletonPage.style.display = 'none'
}

这样封装后,在项目中所有数据请求完毕后就可以通过调用此API隐藏骨架屏,显示原页面了。

5.6、保存提示弹窗原理

为了方便开发,在点击保存后,根据状态的不同,会有相应的弹窗提示,从而让开发人员清楚目前的骨架屏状态,具体实现逻辑如下:
在这里插入图片描述

六、节点编辑器优化

上面的节点样式编辑器虽然可以满足基本的骨架屏样式需求,但是此编辑器除删除节点外都要在devtool中进行操作,降低了开发骨架屏的效率,因此在上述基础上,对现有的编辑器继续添加新功能。

6.1、新增节点推拽功能

在上文中已经说过这个预设:在生成的骨架屏基础上可以对其进行相应的推拽,以调整一些位置略有偏差的节点,并且为了方便节点拖拽的位置把控,采用标线功能,当节点之间距离小于0.5%的时候,就会出现标线。

实现细节,以及遇见问题的解决办法:

这里的元素拖拽采用冒泡,这样就无需遍历页面中的每一个骨架屏节点并添加事件监听了,直接对最外层的父元素去挂监听即可;此外为了方便适配各类尺寸的屏幕,节点定位规则是依据绝对定位+百分比实现的,所以相比于传统的实现拖拽功能,在拖拽时要把相应的top left百分比求出来:

1)记录下鼠标按下的位置:

  offsetX=e.offsetX

  offsetY =e.offsetY    

  clientX=e.clientX

  clientY=e.clientY

2)计算移动偏移量left值:

e.clientX(鼠标移动距离) - offsetX - skeletonDiv.offsetLeft

(减最后一项是因为页面布局利用flex布局,保证窗口缩放也不影响拖拽功能)
计算移动偏移量top值:

   e.clientY(鼠标移动距离) - offsetY - skeletonDiv.offsetTop

减最后一项是因为页面布局利用flex布局,保证窗口缩放也不影响拖拽功能)
核心代码如下:

dragButton.onclick = () => {
  const divParent = document.getElementById('divWrap')
 
  let offsetX = 0
  let offsetY = 0
 
  divParent.onmousedown = function(event) {
    var e = event || window.event
    var target = e.target || e.scrElement
    if (target.className === '_') {   //
      offsetX = e.offsetX
      offsetY = e.offsetY
    }
    document.onmousemove = function(event) {
      var e = event || window.event
      target.style.left = ((e.clientX - 500 - offsetX) / 375) * 100 + '%'
      target.style.top = ((e.clientY - offsetY) / 812) * 100 + '%'
    }
  }
  divParent.onmouseup = function() {
    document.onmousemove = ''
  }
}

但是,这里有一个小问题,因为设定好的事件委托的对象,不仅包含普通的灰色骨架屏div还包含着背景div,这样委托的话,拖动背景也会有相应的改变,这里用一个css样式就可以解决这个问题,只要把背景di的css样式设置一个属性:

pointer-events:none;

但是最初的委托对象是最外层的骨架屏div,这样就会发生点击空白区整个骨架屏都跟随移动,所以这里采取的办法是新加一个div宽高百分百,使他成为事件委托对象,即可完美解决这个问题,实现元素拖拽:

实现效果如下:
请添加图片描述

6.2、新增右键点击节点复制节点功能:

为了解决有些节点没有被识别出来但还不想添加额外的标识再生成一遍,又新加右键点击复制节点的功能,原理如下:

1)取消右键点击节点的默认事件。

2)右键时记录鼠标位置,将菜单的display值由none改为block,并将其定位到鼠标点击处。

3)获取点击的真实节点clone后插入到骨架屏父节点中。

核心代码如下:


```javascript
function customDom() {
  var dom = document.getElementsByClassName('_')
  for (let i = 1; i < dom.length; i++) {
    dom[i].addEventListener('contextmenu', function(e) {
      e.preventDefault()
    })
    dom[i].addEventListener('mouseup', function(e) {
      const skeletonDiv = document.getElementById('skeleton-dev')
      const divParentAuto = document.getElementById('divWrapAuto')
      console.log(dom[i])
      function copy() {
        console.log(this)
        var addNode = dom[i].cloneNode(true)
        addNode.style.top = '50%'
        addNode.style.left = '0%'
        divParentAuto.appendChild(addNode)
      }
      if (!e) e = window.event
      if (e.button == 2) {
        const menu = document.createElement('div')
        const menuCopy = document.createElement('div')
        menu.id = 'menu'
        menuCopy.className = 'menu'
        menuCopy.innerText = '复制节点'
        menu.appendChild(menuCopy)
        skeletonDiv.appendChild(menu)
        // 根据事件对象中鼠标点击的位置,进行定位
        menu.style.left = e.clientX - skeletonDiv.offsetLeft + 'px'
        menu.style.top = e.clientY + 'px'
        // 改变自定义菜单的宽,让它显示出来
        menu.style.width = '70px'
        window.onclick = function() {
          // 绑定在window上,按事件冒泡处理,不会影响菜单的功能
          skeletonDiv.removeChild(menu)
        }
        menuCopy.addEventListener('click', copy, { once: true })
      }
    })
  }
}

这里有要注意的坑点:
1.在将生成的div插入到父div中时要先克隆,再插入、
2.对于此事件绑定的函数要设置只触发一次。{once:true}

实现效果如下:
请添加图片描述

七、优化版本需求

至此初级骨架屏自动生成器就已经开发完毕了,虽然可以满足基本需求,但是依旧有很多可以改进点,改进点如下:

就UI界面而言,样式可以更加精美;
就节点编辑器的功能而言,可以在现在的基础上添加:

①动画
②更改颜色(包括背景色、节点色)
③节点尺寸、拖拽标线

节点个性化编辑:

①节点宽、高、颜色、圆角
②快捷键删除、复制节点

八、优化版本实现原理

8.1、UI界面

将骨架屏生成器的UI界面进行了调整,使得页面更加简洁、美观,UI界面样式如下:
在这里插入图片描述

8.2、动画实现原理

为了更加方便观察骨架屏在项目中的真实效果,所以添加动画功能,实现原理如下:

1)在html中定义两类动画效果 。

2)点击动画效果的时候,获取真实的骨架屏dom节点,并为其添加相应动画类名。

3)按钮文字由横向波纹(经典闪烁)——>停止动画。

4)定义动画flag,并传到后台。

5)点击保存时根据动画flag前端去掉相应类名、后端添加相应动画css。

核心代码如下:

//以闪烁动画为例  
shanshuo.onclick = () => {
    indexTop++
    if (indexTop % 2 == 1) {  //利用%2===1?判断是要停止动画还是开始动画
      let skeDiv = getSkeletonTrueDiv()
      skeDiv.map(item => {
        item.className += ' activeTopAnimation'
      })
      shanshuo.innerText = '停止动画'
      flag = 'shanshuo'
    }
    if (indexTop % 2 == 0) {
      shanshuo.innerText = '经典闪烁'
      var allElem = document.getElementsByTagName('div')
      var divArr = []
      for (var i = 0; i < allElem.length; i++) {
        if (allElem[i].getAttribute('class') === '_ activeTopAnimation') {
          divArr.push(allElem[i])
        }
      }
      divArr.map(item => {
        item.className = '_'
      })
    }
  }
//![请添加图片描述](https://img-blog.csdnimg.cn/direct/8f9c004185864d529cf758352a5bfc97.gif)
保存时利用正则去掉添加的类名

if (leftRight.innerText === '停止动画') {
  divStyle = getLeftAnDivStr()
  divStyle = divStyle.replace(/activeLeftAnimation/g, '')
  divStyle = divStyle.replace(/border: none;/g, '')
  divStyle = divStyle.replace(/cursor: pointer;/g, '')
}

效果如下:

8.3、更换颜色

为了更加方便的个性化制定骨架屏,新加更改节点颜色的功能,原理如下:

1.统一更换节点颜色原理:此处的UI使用了下拉框形式,当点击生成骨架屏节点后,该下拉框可点击,初始定义好常用的几种骨架屏节点颜色(可继续添加颜色种类)

  1)点击想要的颜色(此处显示十六进制颜色代码+颜色样式)

  2)调用方法获取真实骨架屏dom节点。

  3)获取点击的div的innerText作为各节点的backgroundColr。

2.更换背景颜色原理:原理同上,区别在于只是直接获取背景节点。并更换颜色。

效果如下:

请添加图片描述

目前节点尺寸是必须要通过devtool去修改的,这无疑大大降低了开发效率,并且拖拽的时候没有标线会导致节点对齐较为困难,所以新加这两项功能。

在实现这两项功能前,为了方便缩放+拖拽的效果,对每个节点实现点击时添加蓝色边框+8个圆点以实现目标节点更清晰并且8个方向的缩放都可以轻松实现。点击后的节点样式如下:
在这里插入图片描述

1、标线实现原理(源码部分在ve-app-pages/src/utils/dp/drawLine.js下):

1)判断节点的移动方向:

  向左拖拽节点时: clientX - e.clientX > 0( 鼠标按下时所处位置>鼠标移动量 )

  向上拖拽节点时: clientY - e.clientY > 0  ( 鼠标按下时所处位置>鼠标移动量 )

2)计算拖拽对象与当前页面中的所有节点对象的距离是否<=0.5%,若小于则展示标线。在这里判定距离时需要利用节点的移动方向进行优化,可以分为以下几类情况(拖拽节点为current、遍历节点为target):

增加了标线功能的拖拽效果如下:

请添加图片描述

在上文中,已经介绍了在点击节点时会出现八个圆点以便节点向各个方向改变尺寸。具体实现原理如下:

1)记录已点击节点的width为widthDiv、height为heightDiv、left为leftDiv、top为topDiv。

2)根据上述四个值,确定8个圆点的位置。

3)监听每个圆点的mouseup事件,当事件触发后,执行缩放函数(每个节点的缩放函数都不同)

4)缩放函数原理(以左上角圆点为例,其他圆点原理类似):

 记录鼠标横向、纵向移动距离 。

 现有宽高  - (+)移动j距离 。

 根据目前宽高及定位,重新确定各个圆点的位置。

核心代码如下(以左上角的圆点为例):

if (i == 0) {
  let Xdistance = ((e.clientX - skeletonDiv.offsetLeft - leftDiv * skeWidth * 0.01) / skeWidth) * 100
  let Ydistance = ((e.clientY - skeletonDiv.offsetTop - topDiv * skeHeight * 0.01) / skeHeight) * 100
  nowHeight = heightDiv - Ydistance
  nowWidth = widthDiv - Xdistance
  if (nowWidth < 0 || nowHeight < 0) {
    return
  }
  _this.style.height = nowHeight + '%'
  _this.style.width = nowWidth + '%'
  _this.style.top = topDiv + Ydistance + '%'
  _this.style.left = leftDiv + Xdistance + '%'
  _this.style.border = '1px solid #59c7f9'
  for (let i = 0; i < 8; i++) {
    if (i === 0) {
      top = topDiv + Ydistance - 0.155 + '%'
      left = leftDiv + '%'
    }
    if (i === 1) {
      left = leftDiv + nowWidth / 2 + '%'
    }
    if (i === 2) {
      left = leftDiv + nowWidth + '%'
    }
    if (i === 3) {
      top = topDiv + Ydistance - 0.155 + nowHeight / 2 + '%'
      left = leftDiv + nowWidth + '%'
    }
    if (i === 4) {
      top = topDiv + Ydistance - 0.155 + nowHeight + '%'
      left = leftDiv + nowWidth + '%'
    }
    if (i === 5) {
      left = leftDiv + nowWidth / 2 + '%'
    }
    if (i === 6) {
      left = leftDiv + '%'
    }
    if (i === 7) {
      top = topDiv + Ydistance - 0.155 + nowHeight / 2 + '%'
    }
    point[i].style.top = top
    point[i].style.left = left
  }

最终效果展示:

请添加图片描述

3.节点个性化编辑
3.1节点属性编辑面板

这里为了进一步提升骨架屏的开发速度,所以扩加单个节点编辑的功能:点击节点,在侧边显示其宽、高、圆角、以及颜色。实现原理如下:

1)点击节点,记录各属性。

2)根据各属性更新input的placeholder值。

3)点击确定按钮后,获取input中当前的值,添加给节点。

此处原理较为简单,就不再展示实现代码了,实现效果如下:

请添加图片描述

优化前虽然已经实现了节点的复制功能,节点删除功能,但操作起来不够灵活,为了方便编辑节点,将删除节点功能同复制节点功能一样绑定到鼠标右键上。实现原理如下:

①在1.0版本的右键出现菜单中复制节点的的基础上添加删除节点。

②点击删除节点后 dom.parentNode.removeChild(dom)

但在这里要注意一个问题,即dom的获取,因为该工具有着复制节点这一个功能,而在最初挂在监听器时,只是初始化的dom节点挂载监听,所以要注意对新复制出来的节点也要监听右键点击事件,并挂载相应初始化节点所挂载的所有函数。

还要注意删除节点的同时也要把目标节点的边框圆点同时设置display值为none。

实现效果如下:

3.3快捷键实现复制节点(ctrl+v)与删除节点(ctrl+z)

因为前面已经实现了复制节点与删除节点的功能,所以这里就很简单了,原理为:点击节点,鼠标获取真是dom,监听键盘crtl+v即为复制节点、ctrl+z则为删除节点。

九、骨架屏应用于项目中的效果展示

使用前:
请添加图片描述

使用后:
请添加图片描述

十、PS一些细节实现原理+bug修复记录

10.1、 属性含position:fixed;的节点样式展示有误:

原因:遍历节点时,其实是对页面中展示的页面样式的直接遍历,所以此时就要求用来生成骨架屏的页面与设计稿样式一模一样。而为了方便双屏直接观察骨架屏的样式生成情况,所以采用的方案是定义两个div(宽高等于设计稿宽高),但此时就有了一个问题,对于一些样式为position:fixed;的组件就会展示的原项目不符合,如下图:
请添加图片描述

最初想要采取的解决方案:左侧div加一个iframe,以保证样式的准确,但后来发现这样的方案不可行:因为遍历iframe的节点时会有同源限制。所以改用的解决方案是:为这里真正想要的相对的视窗(非windows)添加一个transform:scale(1);属性,这样它的position:fixed;属性就会被拦截下来。效果如下:
在这里插入图片描述

10.2、为生成的骨架屏节点添加移除事件无效

原因:添加事件前需要获取相应的节点,而初始化时页面内还未生成节点,导致获取失败,页面报错。

解决方法:采用曲线救国的方法,新增一个remove按钮,点击后为骨架屏节点挂载click事件,从而实现相应的节点移除。

10.3、读取文件无法获取最新内容。

原因:在将生成的骨架屏写入到page.config.js文件中的时候,需要先读取此文件,然后将生成的骨架屏已字符串的形式作为其对象的一个属性加入,利用require读取时无法获取到最新内容,因为require默认有缓存。

解决方法:读取前先清一下缓存,保证获取的内容是最新内容。

delete require.cache[require.resolve(`../../pages/${domain}/page.config.js`)]

完成此项目的时候,首先是实现了自动化遍历,本想着利用标签实现自定义是很简单的一件事:在遍历节点绘制时通过node.haszoAttribute(‘needskeleton’)

10.4、通过添加标签的方式实现骨架屏生成的实现方式:

直接判断节点是否有相应的属性,没有就过滤掉,有就绘制,但效果并没有如预期般成功过滤不符合的,而是会报错:
在这里插入图片描述

后来检查了原因,发现是在遍历节点的时候节点会被分为不同的类型,如元素、文本,对于元素类型的节点自然可以调用hasAttribute方法,但是对于文本类型的节点自然无法调用该方法,也就会报错了。所以这里就需要在判断是否存在预设的属性前,过滤掉文本属性的节点。那如何区分呢?通过node.nodeType即可准确区分节点类型,当它等于1时,则是所需的元素节点类型,例如<p>和<div>.

if (node.nodeType == 1) {
  //......
  if (node.hasAttribute('needskeleton'){
  //......
  }
}

10.5、防抖设置

设置index变量,监控点击autoStart、customStart的次数,实现点击autoStart后可以再控制台标记后,再点击customStart,叠加生成骨架屏;但反过来不可以。此外也避免误触xxStart按钮,多次生成骨架屏。

10.6、保存到本地的格式

因为利用require读取的是一个对象类型的数据,添加新属性在写回文件时必须利用JSON.stringify转为字符串类型:JSON.stringify(data),这样虽然满足需求,但是写入的格式比较乱,各属性间没有换行。于是查阅

JSON.stringify(data)的文档看到,他其实可以接受三个参数,指定序列化的形式,于是改用JSON.stringify(data, null, ‘\t’),格式化写入内容。

10.7、弹窗实现细节:

为了方便开发,点击保存按钮时会有相应的弹窗提示,而根据本地是否已经存在骨架屏,所渲染出来的弹窗样式也是不一样的。

对于初始化时就直接渲染一个返回按钮提示即可,如下图:

在这里插入图片描述

对于已经存在骨架屏时,会询问是否覆盖,这是要渲染两个按钮,如下图:

在这里插入图片描述

然后对于确定这个按钮再发起一个post请求,把生成的样式发送到服务器并确定,弹窗实现核心代码:

 var contentHtml = `
    <div class="modal-dialog-content" id="modal-dialog-content">
    <div id='modal-dialog-text'>${res.content}</div>
    <div id='modal-dialog-button-wrap'>
      <button id="dialog-close-button">返回</button>
      <button style="display: none;" id='dialog-confirm-button'>确定</div>
    </div>
  </div
`
    dialogUtil.init({
      id: DIALOG_ID,
      contentHtml
    })
    const closeButtonEle = document.querySelector('#dialog-close-button')
    closeButtonEle.onclick = function() {
      dialogUtil.close(DIALOG_ID)
    }

通过模板去渲染,然后为按钮挂载相应的事件。

所以最初的实现方案就是,定义两种不同模板,针对状态的不同调用不同的模板,实现如上效果;但实操了一下并不能实现,若第一次调用的模板只有一个按钮,第二次点击保存的时候,获取确定这个按钮节点就会报错,并并不会成功渲染,这是因为初始化的时候已经把相关代码注入到html中,此时只有一个按钮,在此点击并不会更改弹窗的节点结构了,所以自然获取不到相应的节点。于是采用如下办法:

初始化就渲染两个按钮,但是将确定这个按钮设置为display:none;当页面有所改动想要重新保存时,再将其display设置为block,成功实现预期效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值