SVG-Ladeanzeige auf Vue.js.

Hallo! Ich studiere am Frontend und entwickle parallel dazu im Trainingsprojekt SPA auf Vue.js für das Backend, das Daten vom Suchbot sammelt. Der Bot generiert 0 bis 500 Einträge, und ich muss: hochladen, nach den angegebenen Kriterien sortieren, in der Tabelle anzeigen.


Weder das Backend noch der Bot können Daten sortieren, daher muss ich alle Daten herunterladen und auf der Browserseite verarbeiten. Das Sortieren ist sehr schnell, aber die Download-Geschwindigkeit hängt von der Verbindung ab, und die angegebenen 500 Datensätze können von 10 bis 40 Sekunden geladen werden.


Beim Laden zeigte ich zunächst einen Spiner, dessen Nachteil darin besteht, dass der Benutzer nicht weiß, wann der Download beendet wird. In meinem Fall ist die Anzahl der Datensätze, die der Bot gefunden hat, im Voraus bekannt, sodass Sie anzeigen können, wie viele Prozent der Datensätze geladen sind.


Um das Warten auf den Benutzer zu erleichtern, habe ich beschlossen, ihm den Ladevorgang zu zeigen:


  1. Ziffern - wie viele Prozent der Datensätze sind bereits geladen
  2. Zeitplan - Ladezeit jedes Datensatzes
  3. Füllung -% Last. Da das Diagramm beim Laden einen rechteckigen Block ausfüllt, ist klar, welcher Teil des Blocks noch gefüllt werden muss

Hier ist die Animation des Ergebnisses, das ich angestrebt und erhalten habe:



... meiner Meinung nach hat es sich als lustig herausgestellt.


In dem Artikel werde ich Ihnen Schritt für Schritt zeigen, wie Sie zum Ergebnis gelangen. Ich habe vor dem Dorf keine Funktionsgraphen im Browser gezeichnet, daher brachte mir die Entwicklung des Indikators einfache, aber neue Erkenntnisse über die Verwendung von SVG und Vue.



Auswählen einer Canvas- oder SVG-Rendering-Methode


Ich habe Canvas in einem einfachen Schlangenspiel auf JS und SVG verwendet. In einem Projekt habe ich es einfach in die Seite mit dem Objekt- Tag eingefügt und festgestellt, dass SVG-Bilder beim Skalieren immer die Schärfe behalten (deshalb war es ein Vektor) und Canvas beobachtet hat Bild verwischen. Aufgrund dieser Beobachtung habe ich beschlossen, ein Diagramm mit SVG zu zeichnen, da Sie irgendwann beginnen müssen.


Arbeitsplan


Basierend auf dem ausgewählten Vue-Framework und der ausgewählten Methode zur Bilderzeugung mit SVG habe ich mir den folgenden Arbeitsplan erstellt:


  1. Suchen und studieren Sie Informationen zum Thema Verwendung von SVG mit Vue
  2. Experimente zur Bildung und Veränderung von SVG im Kontext von Vue
  3. Prototyp-Ladeanzeige
  4. Zuordnung der Ladeanzeige zu einer separaten Vue-Komponente
  5. Anwendung der Komponente im SPA

Erste Schritte


  1. Erstellen eines Projektrohlings

    Ich habe vue cli installiert . Um ein neues Projekt zu erstellen, gebe ich an der Eingabeaufforderung vue create loadprogresser ein , wähle standardmäßig die Projekteinstellungen aus, ein neues vue-Projekt mit dem Namen loadprogresser wird erstellt, und entferne dann das Unnötige daraus:


    WarIst geworden

    Standardmäßig Projektstruktur



    Die Struktur nach der "Reinigung"



    Grüße aus Vue


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

    Mein Text ist "Progresser" in App.vue




  2. Suchen und studieren Sie Informationen zum Thema SVG in Verbindung mit Vue


    Tolle Seite mit nützlichen Informationen zu HTML, CSS und SVG css.yoksel.ru Ein gutes Beispiel für SVG finden Sie in der Dokumentation von Vue selbst. SVG-Diagramm Beispiel und ein solcher Link . Basierend auf diesen Materialien wurde die minimale Komponentenvorlage mit SVG geboren, von der ich ausgehe:


     <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. Experimente zur Bildung und Veränderung von SVG im Kontext von Vue


    SVG Rechteck Rect

    rect - ein Rechteck, die einfachste Figur. Ich erstelle SVG mit den Abmessungen 100x100px und zeichne ein Rechteck mit den Anfangskoordinaten 25:25 und den Größen 50x50px. Die Standardfüllfarbe ist Schwarz (kein Styling).



    SVG-Styling und Hover-Pseudoklasse:

    Ich werde versuchen, das Rechteck rect in svg zu stylen. Dazu füge ich die Klasse "sample" zu svg hinzu. Im Stilabschnitt der vue-Datei füge ich die Stile .sample rect (ich färbe das rechteckige Rechteck mit gelb ein) und .sample rect: hover hinzu, die das rect-Element stilisieren, wenn Sie den Mauszeiger darüber halten:


    Quellcode
     <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-Implementierung


    Fazit: svg passt perfekt in die Template-Vue-Datei und ist mit den vorgeschriebenen Styles gestaltet. Ein Anfang wurde gemacht!


    SVG-Pfad als Basis des Indikators

    In diesem Abschnitt werde ich rect durch path ersetzen, <path :d="D" class="path"/> im d-Attribut des path-Tags. Ich werde die Zeichenfolge D mit den Koordinaten des Pfades von vue übergeben. Die Verbindung wird über v-bind:d="D" , was abgekürzt wird als :d="D"


    Linie D = „M 0 0 0 50 50 50 50 0 Z“ zeichnet drei Linien mit den Koordinaten 0: 0-> 0: 50-> 50: 50-> 0:50 und schließt die Kontur mit dem Befehl Z und bildet ab 50x50px ein Quadrat Koordinate 0: 0. Bei Verwendung des Pfadstils erhält die Form eine gelbe Füllfarbe und einen grauen Rand von 1 Pixel.


    Gelbe Pfadquelle
      <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. Prototyp-Ladeanzeige


    In der Minimalversion habe ich ein einfaches Diagramm erstellt. Ein SVG-Container mit einer Höhe von 100 Pixel und einer Breite von 400 Pixel wird in die Vorlage eingefügt. Darin wird ein Pfad-Tag platziert, zu dessen Attribut d ich die generierte Pfadzeichenfolge d aus den Vue-Daten hinzufüge, die wiederum aus dem TimePoints-Array gebildet wird, in dem jeweils eines von 400 hinzugefügt wird Breite des Containers) eine Zufallszahl im Bereich von 0 bis 100. Alles ist einfach. Im erstellten Lebenszyklus-Hook wird die Aktualisierungsmethode aufgerufen, bei der neue (zufällige) Punkte über die addTime-Methode zum Diagramm hinzugefügt werden. Anschließend gibt die getSVGTimePoints-Methode eine Zeichenfolge zurück, die an PATH übertragen werden soll setTimeout startet die Aktualisierungsmethode neu



    Mehr zur Stringbildung für PATH


    Die Zeichenfolge für PATH wird in der Methode getSVGTimePoints aus dem Array timePoints gebildet, das ich mit reduziere. Als Anfangswert für "Reduzieren" verwende ich "M 0 0" (Beginn bei Koordinate 0: 0). Bei der Reduzierung werden der Linie neue Paare der relativen Koordinaten dX und dY hinzugefügt. Der Großbuchstabe "l" ist dafür verantwortlich, dass die Koordinaten relativ sind (das große "L" gibt die absoluten Koordinaten an), nachdem "l" durch Leerzeichen getrennt dX und dann dY platziert wurde. In diesem Prototyp bewegt sich dY = 1 (Inkrement um 1 Pixel) in Zukunft entlang der X-Achse I mit dem Inkrement dX, das aus der Breite des Containers und der Anzahl der Punkte berechnet wird, die darin platziert werden müssen. In der letzten Zeile der PATH-Generierung
    path +=`L ${this.timePoints.length} 0`
    Ab dem letzten Punkt beende ich den Bau der Linie zur X-Achse. Wenn Sie die Kontur schließen müssen, können Sie am Ende der Linie „Z“ hinzufügen. Zuerst dachte ich, dass die resultierende Zahl ohne eine geschlossene Kontur nicht gefüllt (gefüllt) werden würde, aber dies stellte sich als falsch heraus. Wenn es nicht geschlossen ist, wird kein Strich gezeichnet.


     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 }, 

    Ich werde weiterhin Änderungen vornehmen. Mein Indikator sollte in Breite und Höhe skaliert werden, damit alle übertragenen Punkte in den angegebenen Container passen. Wenden Sie sich dazu an das DOM und ermitteln Sie die Abmessungen des Containers


    1. Informationen zu einem DOM-Element abrufen

      Dem div-Container (in den svg eingefügt ist) füge ich eine Wrapper-Klasse hinzu, um Breite und Höhe durch Stile zu übergeben. Und damit svg den gesamten Containerraum einnimmt, setzt es seine Höhe und Breite auf 100%. RECT nimmt wiederum den gesamten Containerraum ein und ist der Hintergrund für 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> 

      Um meinen DIV-Container im virtuellen DOM Vue zu finden, füge ich das ref-Attribut hinzu und gebe ihm einen Namen, unter dem ich ref="loadprogresser" suchen ref="loadprogresser" . Im mounted Lifecycle-Hook rufe ich die Methode getScales () auf, bei der ich mit der Zeichenfolge const {width, height} = this.$refs.loadprogresser.getBoundingClientRect() die Breite und Höhe des DIV-Elements finde, nachdem es im DOM angezeigt wird.


      Weitere einfache Berechnungen des Inkrements entlang der X-Achse, abhängig von der Breite des Containers und der Anzahl der Punkte, die in ihn eingefügt werden sollen. Die Skala entlang der Y-Achse wird jedes Mal neu berechnet, wenn das Maximum im übertragenen Wert gefunden wird.



    2. transformieren - Ändere das Koordinatensystem

      In diesem Stadium stelle ich fest, dass das Koordinatensystem so geändert werden muss, dass die 0: 0-Koordinate in der unteren linken Ecke beginnt und die Y-Achse nach oben und nicht nach unten wächst. Sie können natürlich Berechnungen für jeden Punkt durchführen, aber SVG verfügt über ein Transformationsattribut, mit dem Sie Koordinaten transformieren können.


      In meinem Fall muss ich eine Skala von -1 auf die Y-Koordinaten anwenden (damit die Y-Werte festgelegt werden) und den Ursprung auf die Minus- Containerhöhe verschieben. Da die Höhe des Containers beliebig sein kann (über Stile angegeben), mussten wir im mounted Hook eine Koordinatentransformationslinie mit dem folgenden Code bilden: this.transform = `scale( 1, -1) translate(0,${-this.wrapHeight})`


      Die auf PATH allein angewendete Transformation funktioniert jedoch nicht. Dazu müssen Sie PATH in eine Gruppe (g-Tag) einschließen, auf die Koordinatentransformationen angewendet werden:


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

      Infolgedessen wurden die Koordinaten korrekt umgekehrt, und die Download-Anzeige kam dem Design näher



    3. SVG-Text und Textzentrierung

      Der Text wird benötigt, um% load anzuzeigen. Die vertikale und horizontale Platzierung von Text in der Mitte in SVG ist recht einfach zu organisieren (im Vergleich zu HTML / CSS). Attribute helfen (ich schreibe sofort die Werte) dominant-baseeline = "central" und text-anchor = "middle".


      Text in SVG wird mit dem entsprechenden Tag angezeigt:


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

      Dabei ist TextPrc die Bindung an die entsprechende Variable, berechnet durch ein einfaches Verhältnis der erwarteten Anzahl von Punkten zum übertragenen Betrag this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %` this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %` .


      Die Koordinaten des Anfangs x = "50%" y = "50%" entsprechen der Mitte des Containers, und die Attribute "Dominante Grundlinie" und "Textanker" sind dafür verantwortlich, dass der Text vertikal und horizontal ausgerichtet wird.



      Grundlegende Dinge zum Thema wurden ausgearbeitet, jetzt müssen wir den Indikatorprototyp in einer separaten Komponente auswählen.




  5. Zuordnung der Ladeanzeige zu einer separaten Vue-Komponente


    Zunächst werde ich die Daten bestimmen, die ich an die Komponente übertragen werde. Dies sind: maxSamples - die Anzahl der Samples bei 100% Breite und Point - die Dateneinheit (Punkt), die in das Array von Punkten eingegeben wird (basierend darauf, dass sie nach der Verarbeitung gebildet werden Zeitplan). Die vom Elternteil an die Komponente übertragenen Daten platziere ich im Requisitenbereich


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

    Reaktivitätsprobleme

    Die berechnete Eigenschaft getPath ist dafür verantwortlich, dass der an die Komponente übergebene neue Punkt verarbeitet wird, was von Point abhängt (und wenn dies der Fall ist, wird er neu berechnet, wenn sich Point ändert).


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

    Zuerst habe ich einen Punkt vom Typ Nummer gemacht, was logisch ist, aber dann wurden nicht alle Punkte verarbeitet, sondern nur von den vorherigen unterschieden. Wenn beispielsweise nur die Nummer 10 vom übergeordneten Punkt auf einen solchen Punkt übertragen wird, wird nur ein Punkt in der Tabelle gezeichnet. Alle nachfolgenden Punkte werden ignoriert, da sie sich nicht von den vorherigen unterscheiden.


    Das Ersetzen des Punkttyps von Number durch das Objekt {value: 0} führte zum gewünschten Ergebnis - die berechnete Eigenschaft getPath () verarbeitet nun jeden übertragenen Punkt über Point.value. Ich übergebe die Werte der Punkte


    Progresser.vue-Komponentenquelle
     <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> 


    Aufruf von der übergeordneten Komponente und Übergabe von Parametern

    Um mit einer Komponente zu arbeiten, müssen Sie sie in die übergeordnete Komponente importieren
    import Progresser from "./components/Progresser"
    und im Abschnitt deklarieren
    components: {Progresser }


    In die Vorlage der übergeordneten Komponente wird die Progresser-Indikator-Komponente mit der folgenden Konstruktion eingefügt:


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

    Über die Klasse „Progreser“ werden zunächst die Blockgrößen des Indikators eingestellt. MaxSamples (maximale Anzahl von Punkten im Diagramm) aus der übergeordneten Variablen SamplesInProgresser werden an die Komponenten-Requisiten übertragen, und der nächste Punkt (in Form eines Objekts) aus der Punktvariablen des übergeordneten Objekts wird an den Requisitenpunkt übertragen. Der Punkt des Elternteils wird in der Aktualisierungsfunktion berechnet und repräsentiert zunehmende Zufallszahlen. Ich bekomme dieses Bild:



    App.vue übergeordnete Quelle
     <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. Anwendung der Komponente im SPA


    An den Punkt kommen, an dem alles lag. Daher habe ich asynchrone Vorgänge zum Laden von Datensätzen über bestimmte Identitäten aus der Datenbank. Die Ausführungszeit einer asynchronen Operation ist nicht im Voraus bekannt. Ich werde die Ausführungszeit auf triviale Weise mit new Date (). GetTime () vor und nach der Operation messen und die resultierende Zeitdifferenz auf die Komponente übertragen. Natürlich wird der Indikator in den Block eingebaut, der beim Laden angezeigt wird, und verdeckt die Tabelle, für die die Daten geladen werden.


     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 ... 

    In den Daten der übergeordneten Komponente verschreibe ich in Bezug auf die Lastangabe:


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

    Und in der Vorlage:


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


Schlussfolgerungen


Wie vorhergesagt, nichts kompliziertes. Bis zu einem gewissen Punkt können Sie SVG als reguläre HTML-Tags mit eigenen Besonderheiten behandeln. SVG ist ein leistungsstarkes Tool, das ich jetzt häufiger in meiner Arbeit zur Datenvisualisierung verwenden werde


Referenzen


Laden Sie den Indikator-Quellcode herunter
Svg-Pfad Artikel

Source: https://habr.com/ru/post/de460399/


All Articles