Vue.js上的SVG加载指示器

你好 我在前端学习,与此同时在培训项目中,我在Vue.js上为后端开发SPA,该SPA从搜索机器人中收集数据。 该机器人会生成0到500个条目,我必须:上传,按指定条件排序,显示在表格中。


后端和漫游器都无法对数据进行排序,因此我必须下载所有数据并在浏览器端进行处理。 排序速度非常快,但是下载速度取决于连接,指示的500条记录可以在10到40秒内加载。


首先,在加载时,我展示了一个微调器,其缺点是用户不知道下载何时结束。 就我而言,该漫游器发现的记录数是预先已知的,因此您可以显示已加载了多少%的记录。


为了让用户满意,我决定向他展示加载过程:


  1. 数字-已经加载了多少%的记录
  2. Schedule-每个记录的加载时间
  3. 填充-%负载。 由于图形在加载时会填充一个矩形块,因此很明显,该块的哪一部分仍有待填充

这是我想要的结果的动画:



...我认为这很有趣。


在本文中,我将向您展示如何逐步实现结果。 我没有在该村庄之前的浏览器中绘制功能图,因此该指标的开发为我带来了有关SVG和Vue使用的简单但新的知识。



选择画布或SVG渲染方法


我在JS和SVG上的一个简单的蛇游戏中使用Canvas ,在一个项目中,我只是将其插入到object标签的页面中,并注意到在缩放时,SVG图像始终保持清晰(这就是向量),并且观察到了Canvas模糊图像。 基于这种观察,我决定使用SVG绘制图形,因为您必须花点时间开始。


工作计划


根据所选的Vue框架和所选的使用SVG形成图像的方法,我制定了以下工作计划:


  1. 搜索和研究有关将SVG与Vue结合使用的信息
  2. 在Vue中进行SVG的形成和变化的实验
  3. 原型加载指示器
  4. 将加载指示器分配到单独的Vue组件中
  5. 组件在SPA中的应用

开始使用


  1. 创建项目空白

    我已经安装了vue cli 。 要创建一个新项目,请在命令行中输入vue create loadprogresser默认情况下选择项目设置,然后使用名称loadprogresser创建一个新的vue项目,然后从中删除不必要的项目:


    已成为

    默认情况下,项目结构



    “清洗”后的结构



    Vue的问候


    <template> <div> <h1>Progresser</h1> </div> </template> <script> export default {name: 'app'} </script>; 

    我的文字是App.vue中的“ Progresser”




  2. 搜索和研究有关将SVG与Vue结合使用的信息


    伟大的网站,提供有关HTML,CSS和SVG的有用信息css.yoksel.ru Vue本身的文档中提供了SVG的一个很好的示例SVG-graph Example以及此类链接 。 基于这些材料,我从SVG的最小组件模板中诞生了:


     <template> <div class="wrapper"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"> //svg    //   svg- </svg> </div> </template> 

  3. 在Vue中进行SVG的形成和变化的实验


    SVG矩形矩形

    rect-矩形,最简单的图形。 我创建尺寸为100x100px的svg,并绘制一个矩形rect,其初始坐标为25:25,尺寸为50x50 px,默认填充颜色为黑色(无样式)



    SVG样式和悬停伪类:

    我将尝试在svg中设置矩形rect的样式。 为此,我将“ sample”类添加到svg中,在vue文件的样式部分中添加了样式.sample rect(我用黄色为rect矩形上色)和.sample rect:hover,当您将鼠标悬停在其上方时,它将对rect元素进行样式化:


    源代码
     <template> <div id="app"> <svg class="sample" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px"> <rect x=25 y=25 width="50px" height="50px"/> </svg> </div> </template> <script> export default { name: 'app' } </script> <style> .sample rect { fill: yellow; stroke: green; stroke-width: 4; transition: all 350ms; } .sample rect:hover { fill: gray; } </style> 


    JSfiddle实现


    结论:svg非常适合模板vue文件,并具有规定的样式。 已经开始了!


    SVG路径作为指标的基础

    在本节中,我将在路径标签的d属性中将rect替换为path, <path :d="D" class="path"/> ,我将通过字符串从vue传递路径D的坐标。 通过v-bind:d="D"建立连接v-bind:d="D" ,缩写为:d="D"


    D行=“ M 0 0 0 50 50 50 50 0 Z”绘制坐标为0:0-> 0:50-> 50:50-> 0:50的三行,并使用Z命令关闭轮廓,从此开始形成50x50px的正方形坐标0:0。 使用路径样式,将为形状提供黄色填充颜色和1px的灰色边框。


    黄色PATH来源
      <template> <div id="app"> <svg class="sample" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px"> <path :d="D" class="path"/> </svg> </div> </template> <script> export default { name: 'app', data(){ return { D:"M 0 0 0 50 50 50 50 0 Z" } } } </script> <style> .path { fill:yellow; stroke:gray; } </style> 


  4. 原型加载指示器


    在最小版本中,我做了一个简单的图表。 在模板中插入一个高度为100px,宽度为400px的svg容器,并在其中放置一个路径标记,在该标记的d属性中,我添加了来自vue数据的生成的路径字符串d,该字符串又由timePoints数组形成,其中添加了400中的每一个容器的宽度)是一个介于0到100之间的随机数。一切都很简单,在创建的生命周期挂钩中,调用了update方法,在该方法中,新的(随机)点通过addTime方法添加到了图表中,然后getSVGTimePoints方法返回一个字符串,该字符串通过setTimeout重新启动更新方法



    有关PATH字符串形成的更多信息


    PATH的字符串是在getSVGTimePoints方法中形成的,该方法由我用reduce处理的timePoints数组形成。 作为reduce的初始值,我使用“ M 0 0”(从坐标0:0开始)。 此外,在reduce中,新的一对相对坐标dX和dY将添加到该行。 大写字母“ l”负责相对坐标(大的“ L”表示绝对坐标),在“ l”放置dX,然后放置dY后,用空格分隔。 在此原型中,dY = 1(增量为1px),将来,我将沿着X轴移动,增量dX是根据容器的宽度和需要放置的点数计算的。 在PATH生成的最后一行
    path +=`L ${this.timePoints.length} 0`
    我强行从最后一点开始,完成了到X轴的线的构建。如果需要关闭轮廓,可以在该行的末尾添加“ Z”,起初我以为如果没有闭合的轮廓,结果图形将不会被填充(填充),但是事实证明这是错误的,在未关闭的地方,笔画-不会画出笔画。


     getSVGTimePoints:function(){ let predY = 0 let path = this.timePoints.reduce((str, item)=>{ let dY = item - predY predY = item return str + `l 1 ${dY} ` },'M 0 0 ') path +=`L ${this.timePoints.length} 0`// Z`     return path }, 

    我将继续进行更改。 我的指标应该在宽度和高度上缩放,以便所有传递的点都适合给定的容器。 为此,请转到DOM并找出容器的尺寸


    1. ref-获取有关DOM元素的信息

      我向div容器(在其中插入了svg)中添加了一个包装器类,以通过样式传递宽度和高度。 这样svg会占据整个容器空间,并将其高度和宽度设置为100%。 反过来,RECT也将占据整个容器空间,并将成为PATH的背景


       <div id="app" class="wrapper" ref="loadprogresser"> <svg id="sample" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"> <rect x=0 y=0 width="100%" height="100%"/> <path :d="d" fill="transparent" stroke="black"/> </svg> </div> 

      为了在虚拟DOM Vue中找到我的DIV容器,我添加了ref属性,并为其指定了一个名称,通过该名称我将搜索ref="loadprogresser" 。 在已mounted生命周期挂钩中,我将调用getScales()方法,其中使用字符串const {width, height} = this.$refs.loadprogresser.getBoundingClientRect()在DIV元素出现在DOM中后const {width, height} = this.$refs.loadprogresser.getBoundingClientRect()我会找到它的宽度和高度。


      沿X轴的增量的进一步简单计算取决于容器的宽度和我们要放入容器中的点数。 每次在传输值中找到最大值时,都会重新计算沿Y轴的比例。



    2. 变换-更改坐标系

      此时,我注意到我们需要更改坐标系,以使0:0坐标从左下角开始,并且Y轴向上而不是向下生长。 当然,您可以为每个点进行计算,但是SVG具有允许您变换坐标的transform属性。


      在我的情况下,我需要对Y坐标应用-1的比例(以便将Y值重新放置),并将原点移动到容器高度。 由于容器的高度可以是任何高度(通过样式指定),因此我们必须使用以下代码在已mounted挂钩中形成坐标转换线: this.transform = `scale( 1, -1) translate(0,${-this.wrapHeight})`


      但是仅将变换应用于PATH无效,为此,您需要将PATH包裹在要应用坐标变换的组(g标签)中:


       <g :transform="transform"> <path :d="d" fill="transparent" stroke="black"/> </g> 

      结果,坐标正确反转,下载指示器变得更接近设计



    3. SVG文字和文字居中

      需要显示文本以显示%load。 在SVG中将文本垂直和水平放置在中心位置非常容易组织(与HTML / CSS相比),属性可以提供帮助(我立即写出值)控制基准=“ central”,text-anchor =“ middle”


      SVG中的文本与相应的标签一起显示:


      <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle">{{TextPrc}}</text>

      其中,TextPrc是与相应变量的绑定,通过预期点数与转移金额的简单比率this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %` this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %`


      开头x =“ 50%” y =“ 50%”的坐标对应于容器的中心,并且主导基线和文本锚属性负责将文本垂直和水平对齐。



      有关该主题的基础知识已经解决,现在我们需要在单独的组件中选择指标原型。




  5. 将加载指示器分配到单独的Vue组件中


    首先,我将确定要传输到组件的数据,这些数据将是:maxSamples-100%宽度处的采样数,Point-将输入点数组的数据单位(点)(在处理后将基于该数据单位形成)时间表)。 从父级传输到组件的数据,我放置在props部分中


     props:{ maxSamples: {//-   100%-  type: Number, default: 400 }, Point:{//  value:0 } } 

    反应性问题

    计算所得的属性getPath负责处理传递给组件的新点,该点取决于Point(如果是,则在Point更改时会重新计算)


      // ... <path :d="getPath"/> ... //  props:{ ... Point:{ value:0 } //  computed:{ getPath(){ this.addValue({value:this.Point.value}) return this.getSVGPoints()//this.d } }, 

    首先,我创建了一个Number类型的Point,这是合乎逻辑的,但是并不是所有的点都经过处理,只是与之前的点不同。 例如,如果仅将数字10从父级转移到这样的一个Point,则在图表上只会绘制一个point,所有后续的point将被忽略,因为它们与先前的没有区别。


    用对象{value:0}将Number中的Point类型替换为期望的结果-计算属性getPath()现在通过Point.value处理每个传输的点,我传递这些点的值


    Progresser.vue组件源
     <template> <div class="wrapper" ref="loadprogresser"> <svg class="wrapper__content" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" > <g :transform="transform"> <path :d="getPath"/> </g> <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle"> {{TextPrc}} </text> </svg> </div> </template> <script> export default { props:{ maxSamples: {//-   100%-  type: Number, default: 400 }, Point:{ value:0 } }, data(){ return { Samples:0,//   wrapHeight:100,//      maxY:0,//  Y  (  ) scales:{ w:1,//   (   ) h:1 //   //(   Y   ) }, Points:[],//     (  Point.value) transform:'scale( 1, -1) translate(0,0)', TextPrc: '0%' } }, mounted: function(){ this.getScales() }, methods:{ getScales(){ const {width, height} = this.$refs.loadprogresser.getBoundingClientRect() //.    this.scales.w = width / this.maxSamples //  Y    this.wrapHeight = height this.transform = `scale( 1, -1) translate(0,${-this.wrapHeight}) rotate(0)` }, getVScale(){//    this.scales.h = (this.maxY == 0)? 0 : this.wrapHeight / this.maxY }, //          getYMax({value = 0}){ this.maxY = (value > this.maxY) ? value : this.maxY }, addValue({value = 0}){ if (this.Samples < this.maxSamples) { this.getYMax({value}) this.getVScale() this.Points.push(value) // Int this.Samples ++; this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %` } }, getSVGPoints(){ //    Path let predY = 0 let path = this.Points.reduce((str, item)=>{ let dY = (item - predY) * this.scales.h predY = item return str + `l ${this.scales.w} ${dY} ` },'M 0 0 ') path +=`L ${this.Points.length * this.scales.w} 0 `// Z`     return path }, }, computed:{ getPath(){ this.addValue({value:this.Point.value})//   return this.getSVGPoints()//this.d -  SVG PATH } } } </script> <style scoped> .wrapper { width: 400px;/*     */ height: 100px;/*  ( )   */ font-size: 4rem; font-weight: 600; border-left: 1px gray solid; border-right: 1px gray solid; overflow: hidden; } .wrapper__content path { opacity: 0.5; fill: lightgreen; stroke: green; stroke-width: 1; } .wrapper__content text { opacity: 0.5; } </style> 


    从父组件调用并传递参数

    要使用组件,您需要将其导入父组件
    import Progresser from "./components/Progresser"
    并在部分中声明
    components: {Progresser }


    在父组件的模板中,使用以下结构插入进度指示器组件:


     <progresser class="progresser" :maxSamples = "SamplesInProgresser" :Point = "Point" ></progresser> 

    通过“ progreser”类,首先设置指标的块大小。 来自SamplesInProgresser父变量的MaxSamples(图形中的最大点数)被传输到props组件,并且父对象的Point变量的下一个点(作为对象)被传递到props Point。 父母的分数是在更新函数中计算的,代表递增的随机数。 我得到这张照片:



    App.vue父源
     <template> <div> <progresser class="progresser" :maxSamples = "SamplesInProgresser" :Point = "Point" ></progresser> </div> </template> <script> import Progresser from "./components/Progresser" export default { name: 'app', data(){ return { SamplesInProgresser:400,// -  Point:{value:0},//"" index:0, // -   TimeM:100 //     } }, created: function () { this.update() }, methods:{ update(){ if (this.index < this.SamplesInProgresser) { this.index++; this.Point = {value:(this.TimeM*Math.random() | 0)} this.TimeM *= 1.01 setTimeout(this.update, 0) } } }, components: { Progresser } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; margin-top: 60px; } /*     */ .progresser { width: 300px; height: 80px; } </style> 


  6. 组件在SPA中的应用


    达到一切为止。 因此,我有异步操作,用于从数据库中加载有关某些身份的记录。 异步操作的执行时间事先未知。 我将在操作之前和之后使用new Date()。GetTime()来简单地测量执行时间,并将产生的时间差传递给组件。 自然地,该指示符将内置在将在加载阶段出现的块中,并模糊正在为其加载数据的表。


     async getCandidatesData(){ ... this.LoadRecords = true //   ,        ... this.SamplesInProgresser = uris.length //      ... for (let item of uris) {// uris  URL    try { const start = new Date().getTime()//   candidate = await this.$store.dispatch('GET_CANDIDATE', item) const stop = new Date().getTime()//   this.Point = {value:(stop-start)}//   Point ... 

    在父组件的数据中,我规定了负载指示:


     data (){ return { ... //  LoadRecords:false, SamplesInProgresser:400, Point:{value:0} } 

    并在模板中:


     <!--   --> <div class="wait_loading" v-show="LoadRecords"> <progresser class="progresser" :maxSamples = "SamplesInProgresser" :Point = "Point" ></progresser> </div> 


结论


如预料的,没有什么复杂的。 到目前为止,您可以将SVG视作具有自己特定特征的常规HTML标签。 SVG是一个功能强大的工具,现在我将在工作中更多地使用它进行数据可视化


参考文献


下载指标源代码
SVG路径文章

Source: https://habr.com/ru/post/zh-CN460399/


All Articles