Indicador de carregamento SVG no Vue.js

Oi Estudo no front-end e, paralelamente, no projeto de treinamento, desenvolvo o SPA no Vue.js para o back-end, que coleta dados do bot de pesquisa. O bot gera de 0 a 500 entradas, e eu tenho que: fazer upload, classificar pelos critérios especificados, mostrar na tabela.


Nem o back-end nem o bot podem classificar dados, então eu tenho que baixar todos os dados e processá-los no lado do navegador. A classificação é muito rápida, mas a velocidade do download depende da conexão e os 500 registros indicados podem ser carregados de 10 a 40 segundos.


No início, ao carregar, mostrei um spiner, cuja desvantagem é que o usuário não sabe quando o download será encerrado. No meu caso, o número de registros que o bot encontrado é conhecido antecipadamente, para que você possa mostrar quantos% de registros são carregados.


Para alegrar a espera do usuário, decidi mostrar a ele o processo de carregamento:


  1. dígitos - quantos% de registros já estão carregados
  2. agenda - tempo de carregamento de cada registro
  3. enchimento -% de carga. Como o gráfico preenche um bloco retangular enquanto ele carrega, fica claro qual parte do bloco ainda precisa ser preenchida

Aqui está a animação do resultado que eu estava buscando e obtive:



... na minha opinião, ficou engraçado.


No artigo, mostrarei como avançar passo a passo em direção ao resultado. Como não desenhei gráficos de funções no navegador antes da vila, o desenvolvimento do indicador me trouxe conhecimento simples, mas novo, sobre o uso do SVG e do Vue.



Escolhendo um método de renderização Canvas ou SVG


Usei o Canvas em um jogo simples de cobra em JS e SVG, em um projeto, apenas o inseri na página com a tag object e notei que, ao escalar, as imagens SVG sempre mantinham nitidez (é por isso que era um vetor) e o Canvas observava desfocar a imagem. Com base nessa observação, decidi desenhar um gráfico usando SVG, porque você precisa começar um dia.


Plano de trabalho


Com base na estrutura selecionada do Vue e no método selecionado de formação de imagem usando SVG, fiz o seguinte plano de trabalho:


  1. Pesquise e estude informações sobre o tópico do uso do SVG em conjunto com o Vue
  2. Experimentos com a formação e mudança de SVG no contexto de Vue
  3. Indicador de carregamento do protótipo
  4. Alocação do indicador de carregamento em um componente Vue separado
  5. Aplicação de componente no SPA

Introdução


  1. Criando um projeto em branco

    Eu tenho o vue cli instalado . Para criar um novo projeto, no prompt de comando, insiro vue create loadprogresser , selecione as configurações do projeto por padrão , um novo projeto vue é criado com o nome loadprogresser e removo o desnecessário:


    WasTornou-se

    Estrutura do projeto por padrão



    A estrutura após a "limpeza"



    Saudações de Vue


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

    Meu texto é "Progresser" no App.vue




  2. Pesquise e estude informações sobre o tópico do uso do SVG em conjunto com o Vue


    Ótimo site com informações úteis sobre HTML, CSS e SVG css.yoksel.ru Um bom exemplo do SVG está disponível na documentação do próprio Vue: Exemplo do SVG-graph e um link . Com base nesses materiais, nasceu o modelo de componente mínimo com SVG a partir do qual começo:


     <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. Experimentos com a formação e mudança de SVG no contexto de Vue


    Retângulo SVG ret

    rect - um retângulo, a figura mais simples. Eu crio svg com dimensões 100x100px e desenho um retângulo com as coordenadas iniciais 25:25 e tamanhos 50x50 px, a cor de preenchimento padrão é preta (sem estilo)



    Estilo SVG e pseudo-classe suspensa:

    Vou tentar estilizar o retângulo em svg. Para fazer isso, adiciono a classe "sample" ao svg, na seção style do arquivo vue adiciono os estilos .sample rect (coro o retângulo com amarelo) e .sample rect: hover que estiliza o elemento rect quando você passa o mouse sobre ele:


    Código fonte
     <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> 


    Implementação JSfiddle


    Conclusão: svg se encaixa perfeitamente no arquivo vue do modelo e é estilizado com os estilos prescritos. Um começo foi feito!


    Caminho SVG como base do indicador

    Nesta seção, substituirei rect por path, <path :d="D" class="path"/> no atributo d da tag path, passarei a string D com as coordenadas do path do vue. A conexão é feita através do v-bind:d="D" , que é abreviado como :d="D"


    A linha D = "M 0 0 0 50 50 50 50 0 Z" desenha três linhas com as coordenadas 0: 0-> 0: 50-> 50: 50-> 0:50 e fecha o contorno com o comando Z, formando um quadrado de 50x50px a partir de coordenada 0: 0. Usando o estilo do caminho, a forma recebe uma cor de preenchimento amarelo e uma borda cinza de 1px.


    Fonte amarela do CAMINHO
      <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. Indicador de carregamento do protótipo


    Na versão mínima, fiz um diagrama simples. Um contêiner svg com uma altura de 100px, uma largura de 400px é inserida no modelo, uma tag de caminho é inserida no interior, ao atributo d ao qual adiciono a string de caminho gerada d a partir dos dados do vue, que por sua vez é formado a partir da matriz timePoints, em que, a cada 10 ms, um dos 400 é adicionado (por a largura do contêiner) um número aleatório no intervalo de 0 a 100. Tudo é simples, no gancho do ciclo de vida criado, o método de atualização é chamado no qual novos pontos (aleatórios) são adicionados ao diagrama por meio do método addTime e, em seguida, o método getSVGTimePoints retorna uma sequência de caracteres para passar para PATH, por meio de setTimeout reinicia o método de atualização



    Mais sobre formação de strings para PATH


    A cadeia de caracteres para PATH é formada no método getSVGTimePoints, a partir da matriz timePoints que eu processo com reduzir. Como valor inicial de redução, eu uso "M 0 0" (comece na coordenada 0: 0). Além disso, na redução, novos pares de coordenadas relativas dX e dY serão adicionados à linha. A letra maiúscula “l” é responsável pelas coordenadas serem relativas (o “L” grande indica as coordenadas absolutas), depois que “l” é colocado dX e depois dY, separados por espaços. Neste protótipo, dY = 1 (incremento de 1px), no futuro, ao longo do eixo X, moverei-me com o incremento dX calculado a partir da largura do contêiner e o número de pontos que precisam ser colocados nele. Na última linha da geração PATH
    path +=`L ${this.timePoints.length} 0`
    Forço, a partir do último ponto, termino de construir a linha no eixo X. Se você precisar fechar o contorno, poderá adicionar "Z" ao final da linha, primeiro pensei que sem um contorno fechado, a figura resultante não seria preenchida (preenchimento), mas isso estava errado, onde não estiver fechado, o golpe - o golpe não será desenhado.


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

    Vou continuar a fazer alterações. Meu indicador deve ser dimensionado em largura e altura para que todos os pontos transmitidos se ajustem ao contêiner especificado. Para fazer isso, vá para o DOM e descubra as dimensões do contêiner


    1. ref - obtendo informações sobre um elemento DOM

      No contêiner div (no qual o svg é inserido), adiciono uma classe de wrapper para passar a largura e a altura pelos estilos. E para que o svg ocupe todo o espaço do contêiner, defina sua altura e largura para 100%. O RECT, por sua vez, também ocupará todo o espaço do contêiner e será o pano de fundo para o 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> 

      Para encontrar meu contêiner DIV no DOM Vue virtual, adiciono o atributo ref e ref="loadprogresser" nome pelo qual procurarei ref="loadprogresser" . No gancho do ciclo de vida mounted , chamarei o método getScales (), no qual, com a sequência const {width, height} = this.$refs.loadprogresser.getBoundingClientRect() a largura e a altura do elemento DIV depois que ele aparece no DOM.


      Além disso, cálculos simples do incremento ao longo do eixo X, dependendo da largura do contêiner e do número de pontos que queremos ajustar nele. A escala ao longo do eixo Y é recalculada cada vez que o máximo é encontrado no valor transmitido.



    2. transformar - alterar o sistema de coordenadas

      Nesse ponto, percebo que precisamos alterar o sistema de coordenadas para que a coordenada 0: 0 comece no canto inferior esquerdo e o eixo Y cresça para cima e não para baixo. Obviamente, é possível fazer cálculos para cada ponto, mas o SVG possui um atributo de transformação que permite transformar coordenadas.


      No meu caso, preciso aplicar uma escala de -1 às coordenadas Y (para que os valores Y sejam recuperados) e mudar a origem para a altura negativa do contêiner. Como a altura do contêiner pode ser qualquer (especificada por meio de estilos), tivemos que formar uma linha de transformação de coordenadas no gancho mounted com o seguinte código: this.transform = `scale( 1, -1) translate(0,${-this.wrapHeight})`


      Mas a transformação aplicada somente ao PATH não funcionará, para isso, é necessário agrupar o PATH em um grupo (tag g) ao qual as transformações de coordenadas são aplicadas:


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

      Como resultado, as coordenadas foram revertidas corretamente, o indicador de download ficou mais próximo do design



    3. Texto SVG e centralização de texto

      O texto é necessário para exibir% de carga. A colocação do texto no centro vertical e horizontalmente no SVG é bastante simples de organizar (em comparação com HTML / CSS), os atributos são resgatados (eu imediatamente defino os valores) dominante-linha de base = "central" e texto-âncora = "meio"


      O texto em SVG é exibido com a tag correspondente:


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

      onde TextPrc é a ligação à variável correspondente, calculada por uma simples proporção do número esperado de pontos em relação à quantidade transferida this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %` this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %` .


      As coordenadas do início x = "50%" y = "50%" correspondem ao centro do contêiner, e os atributos de linha de base dominante e âncora de texto são responsáveis ​​por garantir que o texto esteja alinhado vertical e horizontalmente.



      Coisas básicas sobre o tópico foram resolvidas, agora precisamos selecionar o protótipo do indicador em um componente separado.




  5. Alocação do indicador de carregamento em um componente Vue separado


    Para começar, determinarei os dados que transferirei para o componente: maxSamples - o número de amostras com 100% de largura e Point - a unidade de dados (ponto) que será inserida na matriz de pontos (com base na qual, após o processamento, eles serão formados horário). Os dados transmitidos ao componente pelo pai, coloco na seção adereços


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

    Problemas de reatividade

    A propriedade computada getPath é responsável pelo fato de que o novo ponto passado para o componente é processado, o que depende do Point (e, se o fizer, será recalculado quando o Point mudar)


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

    No começo, criei um Point do tipo Number, que é lógico, mas nem todos os pontos foram processados, mas apenas diferentes dos anteriores. Por exemplo, se apenas o número 10 for transferido do pai para esse ponto, apenas um ponto será desenhado no gráfico, todos os subsequentes serão ignorados, pois não diferem dos anteriores.


    Substituir o tipo de ponto de Number pelo objeto {value: 0} levou ao resultado desejado - a propriedade calculada getPath () agora processa cada ponto transmitido, através de Point.value, passo os valores dos pontos


    Origem do componente 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> 


    Chamando a partir do componente pai e passando parâmetros

    Para trabalhar com um componente, você precisa importá-lo para o componente pai
    import Progresser from "./components/Progresser"
    e declarar na seção
    components: {Progresser }


    No modelo do componente pai, o componente indicador progressivo é inserido com a seguinte construção:


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

    Através da classe "progreser", primeiro, os tamanhos de bloco do indicador são definidos. MaxSamples (número máximo de pontos no gráfico) da variável pai SamplesInProgresser são transferidos para os props componentes, e o próximo ponto (na forma de um objeto) da variável Point do objeto pai é transferido para props Point. O ponto dos pais é calculado na função de atualização e representa números aleatórios crescentes. Eu recebo esta imagem:



    Origem pai 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. Aplicação de componente no SPA


    Chegando ao ponto em que tudo estava fazendo. E, portanto, tenho operações assíncronas para carregar registros sobre determinadas identidades do banco de dados. O tempo de execução de uma operação assíncrona não é conhecido antecipadamente. Medirei o tempo de execução de uma maneira trivial, usando o novo Date (). GetTime () antes e após a operação, e transferirei a diferença de tempo resultante para o componente. Naturalmente, o indicador será incorporado no bloco que aparecerá no estágio de carregamento e ocultará a tabela na qual os dados estão sendo carregados.


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

    Nos dados do componente pai, prescrevo em relação à indicação de carga:


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

    E no modelo:


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


Conclusões


Como previsto, nada complicado. Até algum momento, você pode tratar o SVG como tags HTML regulares, com suas próprias especificidades. O SVG é uma ferramenta poderosa que agora usarei com mais frequência no meu trabalho para visualização de dados


Referências


Código-fonte do indicador de download
Artigo Svg-path

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


All Articles