Skip to content

Instantly share code, notes, and snippets.

@lucis
Created March 9, 2020 17:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lucis/d380b7d7fe3315b58dc2ff8166f2d3bf to your computer and use it in GitHub Desktop.
Save lucis/d380b7d7fe3315b58dc2ff8166f2d3bf to your computer and use it in GitHub Desktop.
Store Block Summary

Conhecendo uma app VTEX IO

Introdução

Antes de começar, é necessário relembrar alguns conceitos importantes para uma maior compreensão do fluxo lógico ao desenvolver uma app.

manifest.json

vendor

Define o nome da conta VTEX que está desenvolvendo a app. Essa conta é responsável pela manutenção e distribuição da app (pode ser instalada em outras contas ou somente na própria)

O vendor vtex é utilizado em casos de apps nativas.

name

Identifica o nome da aplicação. Não deve ter caracteres especiais - exceto - - ou caracteres maiúsculos.

version

Identifica a versão atual da app. Para versionamento, utilizamos a especificação Semantic Versioning 2.0.0. O formato do versionamento é bem definido, com o uso de patches, minors e majors.

Abaixo um resumo da especificação:

  • Patches: você deve criar um patch quando está consertando um bug de forma retrocompatível
  • Minors: você deve criar uma versão minor quando adicionar funcionalidade de forma retrocompatível.
  • Majors: você deve criar uma versão major quando você realiza mudanças incompatíveis de API (o que costumamos chamar de breaking changes)

Exemplo: Se uma API que está na versão 2.3.2 e uma nova funcionalidade não tiver breaking changes, você pode atualizar a versão para 2.4.0.

No momento que o deploy é feito, há um worker chamado housekeeper responsável por atualizar a versão automaticamente para todas as contas. No caso de minors e patches, o housekeeper atualiza a app automaticamente em todas as contas, já que as mudanças são retrocompatíveis. Atualizações de majors, no entanto, possuem breaking changes, por isso o housekeeper não atualiza a app em todas as contas; sendo assim, a atualização deve ser feita manualmente.

builders

O desenvolvimento de apps no VTEX IO utiliza o conceito de Code as Configuration (CaC). Este paradigma é abstraído através do campo de builders que facilita o desenvolvimento, abstraindo a configuração de serviços.

Exemplo: para criar uma extensão no painel administrativo criam-se apps que utilizam o builder de admin.

image

Ao linkar a app, portanto, uma pasta de nome correspondente é enviada ao seu builder, que, por sua vez, transforma cada arquivo em configuração para o serviço competente.

dependencies

Uma app pode depender de outras aplicações. Esse campo lista todas as dependências necessárias para o correto funcionamento da app.

Exemplo

No exemplo da estrutura do manifest.json abaixo, é possível observar características mencionadas acima. Em particular, a versão é 0.0.1, onde os números são, respectivamente, major, minor e patch.

{
  "vendor": "vtex",
  "name": "countdown",
  "version": "0.0.1",
  "title": "Countdown",
  "description": "Countdown component",
  "defaultLocale": "pt-BR",
  "builders": {
    "messages": "1.x",
    "store": "0.x",
    "react": "3.x"
  },
  "mustUpdateAt": "2019-04-02",
  "scripts": {
    "postreleasy": "vtex publish --verbose"
  },
  "dependencies": {
    "vtex.styleguide": "9.x",
    "vtex.css-handles": "0.x"
  },
  "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-a pi/master/gen/manifest.schema"
}

Linkando uma app e utilizando-a no tema da loja

Introdução

Para desenvolver um bloco de frente de loja, similar aos que oferecemos nativamente no Store Framework, utilizamos a biblioteca de desenvolvimento de UIs react.

Um pouco sobre tecnologias

É sabido que criar componentes que manipulem estado em react melhora a performance e tende a ser mais fácil, por ser menos verboso que class components. Portanto, nesse curso iremos utilizar sempre function components e hooks e recomendamos que você faça o mesmo sempre que vá começar um projeto novo em react

No VTEX IO, adotamos o typescript como linguagem default para projetos que normalmente utilizariam javascript. Apesar de ser necessário aprender sintaxes novas, acredita-se que o esforço é rapidamente recompensado. Ao utilizar typescript, ganha-se alta previsibilidade de bugs, por oferecer tipagem estática. Além disso, com as IDEs certas, é possível aumentar a velocidade de implementação através de um code completion mais esperto, com a tipagem de objetos no código.

Neste curso, utilizaremos somente typescript. Caso você não tenha familiaridade, será uma excelente oportunidade de experimentar essa linguagem.

Objetivo dessa Etapa

Como você já tem familiaridade com o Store Framework, já sabe que montamos páginas na nossa loja ao compor blocos em JSON, como shelf e sku-selector. Nesta etapa você irá criar um bloco que será utilizado no tema da home page de sua loja.

Boilerplate

Quando você estiver desenvolvendo seu próprio bloco, você pode começar por nosso template de react.

Atividade

  1. No template clonado, vá para o arquivo Countdown.tsx:

    //react/Countdown.tsx
    import React, { Fragment } from 'react'
    
    interface CountdownProps {}
    
    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({}) => {
    return <Fragment></Fragment>
    }
    
    Countdown.schema = {
    title: 'editor.countdown.title',
    description: 'editor.countdown.description',
    type: 'object',
    properties: {},
    }
    
    export default Countdown
  2. Adicione uma tag h1 dentro do nosso componente e declarar o bloco linkar a app no nosso tema.

    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({}) => {
    -    return <Fragment></Fragment>
    +    return (
    +      <Fragment>
    +        <h1>Teste Countdown</h1>
    +      </Fragment>
    +    )
    }

    Para que o componente seja visto funcionando na loja, é preciso linkar a app no tema. Em primeiro lugar, será necessário ter um tema para adicionar a app, para isso, será necessário cloná-lo do Github. Nesse curso, o store-theme será utilizado. Para clonar o repositório, basta executar o seguinte comando:

    git clone https://github.com/vtex-apps/store-theme.git
    
  3. Com o repositório já clonado, vá até a pasta com cd store-theme; linke a app no seu workspace. Em primeiro lugar, para a app ser utilizada no tema, é preciso adicioná-la às suas dependências, que como visto anteriormente, ficam no manifest.json.

    vtex link
    
  4. Adicione ao manifesto do tema "vtex.countdown" como dependência. A versão dela está definida no manifesto da app (0.0.1). Feito isso, o JSON terá mais uma linha, como mostrado abaixo:

    {
        ...
        "dependencies": {
            ...
    +        "vtex.countdown": "0.x",
            ...
        },
        ...
    }
  5. Por fim, é preciso adicionar o bloco na loja. Dentro do arquivo home.jsonc, declare um bloco chamado "countdown".

    {
        "store.home": {
            "blocks": [
                "countdown",
                ...
            ]
            ...
        }
        ...
    }
    

Após o login, o resultado esperado é encontrar um header na home da sua loja, como a imagem abaixo:

image

Tornando o bloco countdown customizável

Introdução

Agora que temos um h1, é possível utilizá-lo para mostrar informações que dependam de uma prop do componente. Para isso, alguns conceitos serão apresentados, já que são necessários para desenvolver uma aplicação.

Conceitos

  • O Hook

    Hooks são funções que permitem conexão aos recursos de ciclo de vida do React. Eles não funcionam dentro de classes e permitem o uso do React com componentes funcionais.

    Exemplo:

    const [count, setCount] = useState(0);
  • Interface para definir as props

    Define as props e também os tipos associados.

    interface CountdownProps {}
  • Definição das configurações de um bloco

    Para que o seu bloco possa aceitar configurações do usuário, é utilizado um JSON schema, que irá gerar um formulário para o Site Editor. Abaixo é possível ver um exemplo de schema:

    // react/Countdown.tsx
    Countdown.schema = {
        title: 'editor.countdown.title',
        description: 'editor.countdown.description',
        type: 'object',
        properties: {},
    }

    Tal schema é responsável, inclusive por definir os textos presentes no formulário em si.

Atividade

  1. Na interface definida no Countdown.tsx, adicione uma prop chamada targetDate, ela é do tipo string. Com isso, estamos definindo uma prop do componente que será utilizada para inicializar o contador.

    A definição da prop em si é feita através da declaração dela na interface CountdownProps no arquivo Countdown.tsx, mostrada anteriormente. Assim, adicione uma linha que defina uma prop chamada targetDate, do tipo string.

    // react/Countdown.tsx
    interface CountdownProps {
    +   targetDate: string    
    }
  2. Feito isso, é preciso utilizá-la no componente, substituindo o texto de antes, "Teste Countdown" por um outro texto, através do Site Editor.

    No futuro, esse targetDate será utilizado para definir a data de término para o contador. Porém, por enquanto, esse campo pode ser genérico.

    Primeiramente, é preciso alterar o componente para utilizar a prop targetDate definida anteriormente. Para isso, é preciso adicionar dentro do componente React a variável a ser utilizada no h1. Você lembra do bloco de código do componente na etapa anterior? Vamos utilizá-lo novamente para fazer as alterações.

    // react/Countdown.tsx
    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate }) => {
        return (
            <Fragment>
                <h1>{ targetDate }</h1>
            </Fragment>
        ) 
    }
  3. Além disso, para alterar essa propriedade através do Site Editor, é necessário adicionar essa mesma prop ao schema. Isso é feito através da adição de um objeto com chave targetDate dentro do objeto properties no schema. Ou seja:

    // react/Countdown.tsx
    Countdown.schema = {
        title: 'countdown.title',
        description: 'countdown.description',
        type: 'object',
        properties: {
    +        targetDate: {
    +            title: 'Sou um título',
    +            description: 'Sou uma descrição',
    +            type: 'string',
    +            default: null,
    +        },
        },
    }

Pronto! Agora você pode alterar o conteúdo do texto através do Site Editor. Vamos ver como ficou? Vá até o Site Editor e clique em Countdown no menu lateral, isso abrirá o menu de edição da app, que será como a imagem abaixo.

Agora, no campo abaixo do título, digite alguma coisa e veja a alteração, que passará a exibir o texto que você digitou.

image

Criando a funcionalidade do bloco countdown

Introdução

Com o básico do nosso componente e funcional, é hora de implementar efetivamente o contador. Para isso, é preciso utilizar um hook do React, chamado useState;

O hook useState

É chamado dentro de um componente funcional para atualizar e consumir o state de um componente. O state simboliza o estado atual de um componente.

O useState retorna um par: o valor do estado atual e uma função para atualizá-lo.

Voltando ao exemplo apresentado na etapa anterior, podemos mostrar na prática os conceitos abordados anteriormente. Para lembrar do exemplo, veja o código abaixo:

const [count, setCount] = useState(0);

No trecho acima é importante observar três coisas:

  • Na variável count, é possível consumir o estado atual;
  • setCount é uma função para atualizá-lo;
  • 0 é o valor do estado inicial
const [timeRemaining, setTime] = useState<TimeSplit>({
    hours: '00', 
    minutes: '00', 
    seconds: '00'
  })

Atividades

  1. Em primeiro lugar, é preciso importar algumas coisas necessárias e a primeira delas é o hook em si. Para isso, no componente, adicione na linha de import a função useState do React:

    import React, { Fragment, useState } from 'react'

    Além disso, é necessário importar o tipo TimeSplit:

    import { TimeSplit } from './typings/global'

    Por fim, é oferecida uma função util que atualizará a contagem regressiva:

    import { tick } from './utils/time'
  2. Adicione o hook de atualização de estado (useState)

    Voltando ao nosso componente Countdown, vamos adicionar o hook:

    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate }) => {
    +    const [timeRemaining, setTime] = useState<TimeSplit>({
    +        hours: '00',
    +        minutes: '00',
    +        seconds: '00'
    +    })
    
        return (
            <Fragment>
                { targetDate }
            </Fragment>
        ) 
    }

    É possível observar alguns detalhes com essa adição: timeRemaining é o estado atual, setTime é a função de atualização do estado, TimeSplit é o tipo e, por fim, o objeto {hours: '00', minutes: '00', seconds: '00'} é o estado inicial do componente.

  3. Adicione uma targetDate padrão para o caso de não haver um valor inicial definido. Para isso, declare uma constante que será utilizada como padrão:

    const DEFAULT_TARGET_DATE = (new Date('2020-03-11')).toISOString()
    
  4. Utilize a função tick e a constante DEFAULT_TARGET_DATE para fazer o contador:

    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate = DEFAULT_TARGET_DATE }) => {
        const [timeRemaining, setTime] = useState<TimeSplit>({
            hours: '00',
            minutes: '00',
            seconds: '00'
        })
    
    +   tick(targetDate, setTime)
    
        return (
            <Fragment>
                { targetDate }
            </Fragment>
        ) 
    }
  5. Altere o h1 para que ele exiba o contador que criamos. Para isso, precisamos utilizar o estado atual timeRemaining:

    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate = DEFAULT_TARGET_DATE }) => {
        const [timeRemaining, setTime] = useState<TimeSplit>({
            hours: '00',
            minutes: '00',
            seconds: '00'
        })
    
    tick(targetDate, setTime)
    
        return (
            <Fragment>   
    -            <h1>{ targetDate }</h1>
    +            <h1>{ `${timeRemaining.hours}:${timeRemaining.minutes}:${timeRemaining.seconds}` }</h1>
            </Fragment>
        ) 
    }

    A formatação da string do contador está no formato HH:MM:SS, feita através do split em hours, minutes e seconds.

Assim, com essas alterações, veremos a atualização em tempo real do contador! O resultado na home é esse aqui:

image

E veja o contador funcionando:

Modificando o bloco countdown para ter um estilo configurável

Introdução

Com uma app funcional, que tal adicionar um pouco de customização? Nessa etapa, você irá aprender conceitos básicos a respeito de CSS handles e Tachyons para, em seguida, customizar sua app.

CSS Handles

Os handles de CSS são utilizados para customizar os componentes da sua loja através de classes de CSS no código do tema. Todas essas configurações são definidas no arquivo styles.json, responsável por declarar todas as customizações genéricas para a sua loja.

Se você der uma olhada na sua loja, perceberá que os componentes tem estilos similares, mesmo sem aplicar nenhum tipo de customização. Isso acontece pois todos compartilham estilos previamente definidos para tipos de fontes, cores de background, formato dos botões e etc.

Todas essas definições podem ser alteradas, de forma que sua loja passe a ter um estilo mais customizado. Para isso, basta definir um arquivo JSON na pasta styles/configs; essas informações podem ser encontradas de forma mais detalhada em: Build a store using VTEX IO - Customizing styles.

Tachyons

O Tachyons é um framework para CSS funcional. Diferentemente de outros frameworks conhecidos, como o Bootstrap, ele não apresenta componentes UI "pré-buildados". Na verdade, seu objetivo é justamente separar as regras de CSS em pequenas e reutilizáveis partes. Esse tipo de estratégia é comumente conhecida como Subatomic Design System e, caso você tenha interesse, pode encontrar uma referência nesse link. Essa estratégia torna frameworks como o Tachyons muito flexíveis, escaláveis e rápidos.

Resumindo, a ideia do CSS funcional é que, ao invés de escrever grandes classes, você escreve pequenas. Essas pequenas classes possuem propriedades únicas e imutáveis, podendo ser combinadas para formar componentes maiores no HTML.

Atividade

  1. Importe o useCssHandles. Para isso, volte ao Countdown.tsx e faça o import:

    // react/Countdown.tsx
    import { useCssHandles } from 'vtex.css-handles'
  2. Além disso, defina a constante do estilo que iremos puxar do handles. Neste caso, o countdown:

    // react/Countdown.tsx
    const CSS_HANDLES = [ 'countdown' ]
  3. Utilize o useCssHandles no componente Countdown para "pegar" o estilo que necessário do countdown e, além disso, troque o Fragment por uma tag de div:

    // react/Countdown.tsx
    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate = DEFAULT_TARGET_DATE }) => {
        const [timeRemaining, setTime] = useState<TimeSplit>({
            hours: '00',
            minutes: '00',
            seconds: '00'
        })
    
    +    const handles = useCssHandles(CSS_HANDLES)
    
        tick(targetDate, setTime)
    
        return (
            <Fragment>
                <h1>
                    { `${timeRemaining.hours}:${timeRemaining.minutes}:${timeRemaining.seconds}` }
                </h1>
            </Fragment>
        )
    }
  4. Por fim, é preciso utilizar tais estilos no componente a fim de vermos a customização. Para isso, é necessário utilizar a prop className com as classes a serem utilizadas e o VTEX Tachyons, para os estilos globais. Além disso, remova o Fragment importado do React para evitar erros no build.

    // react/Countdown.tsx
    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate = DEFAULT_TARGET_DATE }) => {
        const [timeRemaining, setTime] = useState<TimeSplit>({
            hours: '00',
            minutes: '00',
            seconds: '00'
        })
    
        const handles = useCssHandles(CSS_HANDLES)
    
        tick(targetDate, setTime)
    
        return (
            <Fragment>
    +           <div className={`${handles.countdown} t-heading-2 fw3 w-100 c-muted-1 db tc`}>
    +                {`${timeRemaining.hours}:${timeRemaining.minutes}:${timeRemaining.seconds}`}
    +           </div>
            </Fragment>
        )
    }

Vamos ver o resultado?

image

Práticas de internacionalização no VTEX IO

Introdução

Agora que já renderizamos nossos blocos customizados na loja, devemos aprender a internacionalizar o conteúdo que apresentamos.

É importante lembrar que os blocos devem sempre seguir boas práticas de localização, e não devem mostrar strings hardcoded, mas sim sensíveis a linguagem que a loja opera.

Não se preocupe, você não precisará adicionar traduções de todos os textos para as variadas linguagens nas quais o Store Framework é usado. Portanto, nessa etapa, serão apresentados conceitos acerca da internacionalização de apps e como fazê-la.

As Messages

O conceito de messages facilita a adição de novos idiomas a serem configurados para funcionarem como uma opção de idioma para os temas. Considerando isso, todas as apps precisam estar alinhadas em termos de línguas disponíveis, para evitar inconsistências na tradução.

As messages centralizam todos os serviços de tradução na plataforma. Dada um texto a ser traduzido, Messages irá primeiramente checar o contexto definido pelo usuário para, em seguida, checar as traduções das apps e, por fim, passa pelo sistema de tradução automática.

De forma a utilizar tais definições, os campos de string do schema passam a definidos através de valores de chave de um JSON que estão presentes em todos os arquivos mencionados anteriormente, utilizando a versão correta de acordo com a língua configurada.

Na estrutura do diretório, é possível observar que há uma pasta chamada messages, que apresenta três arquivos principais: pt.json, en.json e es.json, cada um responsável pelas devidas traduções: português, inglês e espanhol, respectivamente. Além disso, a fim de fornecer traduções automáticas melhores, utilizamos o arquivo context.json, responsável por evitar ambiguidades.

O arquivo context.json é necessário e precisa conter todas as chaves de tradução para as strings de tradução.

Atividade

Você já deve ter aprendido a usar o nosso builder messages, e também será através dele que adicionaremos strings internacionalizadas em nossos componentes. O primeiro passo para isso é, na pasta messages, adicionarmos as mensagens que queremos exibir nos arquivos das linguagens que existe lá. Vamos, agora, adicionar uma mensagem de título para nosso componente:

messages/pt.json

{
	...,
+	"countdown.title": "Contagem Regressiva"
}

messages/en.json

{
	...,
+	"countdown.title": "Countdown"
}

messages/es.json

{
	...,
+	"countdown.title": "Cuenta Regresiva"
}

messages/context.json

{
	...,
+	"countdown.title": "Countdown"
}

Após isso, para renderizar nosso título devemos usar o componente FormattedMessage da biblioteca react-intl. Não é preciso se preocupar com a configuração da biblioteca, tudo isso é feito pelo nosso framework :

A biblioteca react-intl dá suporte a várias maneiras de configuração e internacionalização, vale a pena verificá-las

  1. Adicione a biblioteca usando yarn add react-intl na pasta react

  2. No código do seu componente Countdown.tsx importe o FormattedMessage

    +	import { FormattedMessage } from 'react-intl'
  3. Adicione uma constante que será o seu título:

    const titleText = title || <FormattedMessage id="countdown.title"/>
  4. Agora, vamos juntar o título e o contador para renderizá-los. Para isso, vamos definir um container por fora. Além disso, o texto do título será passado através da prop title:

    const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ title, targetDate }) => {
      const [
        timeRemaining, 
        setTime
      ] = useState<TimeSplit>({
        hours: '00', 
        minutes: '00', 
        seconds: '00'
      })
      
      const titleText = title || <FormattedMessage id="countdown.title" /> 
      const handles = useCssHandles(CSS_HANDLES)
    
      tick(targetDate, setTime)
    
      return (
        <Fragment>
          <div className={`${handles.container} t-heading-2 fw3 w-100 c-muted-1`}>
            <div className={`${handles.title} db tc`}>
              { titleText }
            </div>
            <div className={`${handles.countdown} db tc`}>
              {`${timeRemaining.hours}:${timeRemaining.minutes}:${timeRemaining.seconds}`}
            </div>
          </div>
        </Fragment>
      )
    }

    Note que utilizamos três handles: container, countdown e title. Dessa forma, lembre-se de declará-los na constante CSS_HANDLES, vista na etapa anterior:

    const CSS_HANDLES = ['container', 'countdown', 'title']

    Por fim, precisamos adicionar a prop de title no schema:

    Countdown.schema = {
      title: 'editor.countdown.title',
      description: 'editor.countdown.description',
      type: 'object',
      properties: {
    +    title: {
    +      title: 'Sou um título',
    +      type: 'string',
    +      default: null,
    +    },
        targetDate: {
          title: 'Sou um título',
          description: 'Sou uma descrição',
          type: 'string',
          default: null,
        },
      },
    }

    Pronto! Agora, para testar sua loja em outros idiomas basta adicionar a query string /?cultureInfo=pt-br na URL, por exemplo. Ao utilizar tal URL, o resultado esperado é esse aqui:

    image

Componentizando o bloco countdown

Introdução

Nessa etapa, temos em nossa app dois pedaços: o título e o contador. Porém, para obter uma maior flexibilidade em termos de posicionamento, customização e etc., é interessante que nós as separemos em dois blocos distintos. Para isso, precisamos apresentar brevemente o conceito de interfaces para, em seguida, desenvolvermos um novo componente Title. Um exemplo de customização em termos de posicionamento, que será abordada nessa etapa, é: e se quiséssemos que nosso título estivesse embaixo ou ao lado do contador?

Interface

Uma interface funciona como um contrato, com restrições bem definidas de como os blocos funcionarão juntos. Define, então, um mapeamento que cria um bloco do Store Framework, a partir de um componente React. É importante destacar que o uso de interfaces, de forma a quebrar nossa app em diversas interfaces associadas a diferentes blocos torna o poder de customização muito maior.

Ao definir a app na interface, a propriedade component é responsável por definir o componente React que será usado. É importante ressaltar que o nome do component tem que ser igual ao nome do arquivo do componente dentro da pasta react/.

Exemplo de interfaces.json:

{
  "countdown": {
    "component": "Countdown"
  }
}

Atividade

Nessa atividade, vamos separar o título e adicionar à nossa loja embaixo do contador. Vamos lá?

Alterando o componente Countdown

No arquivo Countdown.tsx, é preciso remover algumas linhas de código.

  1. Vamos remover os imports, o title da interface e alterar a constante do CSS handles:
    import React, { Fragment, useState } from 'react'
    import { TimeSplit } from './typings/global'
    import { tick } from './utils/time'
    import { useCssHandles } from 'vtex.css-handles'
    -import { FormattedMessage } from 'react-intl'
    
    interface CountdownProps {
      targetDate: string,
    -  title: string
    }
    
    const DEFAULT_TARGET_DATE = (new Date('2020-03-02')).toISOString()
    -const CSS_HANDLES = ['container', 'countdown', 'title']
    +const CSS_HANDLES = ['countdown']
  2. Agora, no componente React em si, precisamos retirar o title como prop recebida e a constante do texto do título, além de alterar o que é renderizá-lo em sim:
    //Countdown.tsx
    -const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ title, targetDate = DEFAULT_TARGET_DATE }) => {
    +const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate = DEFAULT_TARGET_DATE }) => {
      const [
        timeRemaining, 
        setTime
      ] = useState<TimeSplit>({
        hours: '00', 
        minutes: '00', 
        seconds: '00'
      })
      
    -  const titleText = title || <FormattedMessage id="countdown.title" /> 
      const handles = useCssHandles(CSS_HANDLES)
    
      tick(targetDate, setTime)
    
      return (
        <Fragment>
    -      <div className={`${handles.container} t-heading-2 fw3 w-100 pt7 pb6 c-muted-1`}>
    -        <div className={`${handles.title} db tc`}>
    -          { titleText }
    -        </div>
            <div className={`db tc`}>
              {`${timeRemaining.hours}:${timeRemaining.minutes}:${timeRemaining.seconds}`}
            </div>
    -      </div>
        </Fragment>
      )
    }
  3. Por fim, vamos retirar o título do schema:
    Countdown.schema = {
      title: 'editor.countdown.title',
      description: 'editor.countdown.description',
      type: 'object',
      properties: {
    -    title: { 
    -      title: 'editor.countdown.title.title',
    -      type: 'string',
    -      default: null,
    -    },
        targetDate: {
          title: 'editor.countdown.targetDate.title',
          description: 'editor.countdown.targetDate.description',
          type: 'string',
          default: null,
        },
      },
    }

O resultado final de todas essas mudanças é o seguinte:

import React, { Fragment, useState } from 'react'
import { TimeSplit } from './typings/global'
import { tick } from './utils/time'
import { useCssHandles } from 'vtex.css-handles'

interface CountdownProps {
  targetDate: string
}

const DEFAULT_TARGET_DATE = (new Date('2020-03-02')).toISOString()
const CSS_HANDLES = ['countdown']

const Countdown: StorefrontFunctionComponent<CountdownProps> = ({ targetDate = DEFAULT_TARGET_DATE }) => {
  const [timeRemaining, setTime] = useState<TimeSplit>({
    hours: '00',
    minutes: '00',
    seconds: '00'
  })

  const handles = useCssHandles(CSS_HANDLES)
  tick(targetDate, setTime)

  return (
    <Fragment>
      <div className={`${handles.countdown} t-heading-2 fw3 w-100 c-muted-1 db tc`}>
        {`${timeRemaining.hours}:${timeRemaining.minutes}:${timeRemaining.seconds}`}
      </div>
    </Fragment>
  )
}

Countdown.schema = {
  title: 'editor.countdown.title',
  description: 'editor.countdown.description',
  type: 'object',
  properties: {
    targetDate: {
      title: 'editor.countdown.targetDate.title',
      description: 'editor.countdown.targetDate.description',
      type: 'string',
      default: null,
    },
  },
}

export default Countdown

Criando um novo componente

Crie um novo arquivo dentro da pasta /react, chamado Title.tsx, ele será o novo componente do título. Nele, alguns imports precisam ser feitos. A estrutura básica do código é muito similar a do componente Countdown.

Agora, vamos colocar a mão na massa e criar nosso novo componente! Em um primeiro momento, vamos começar com o esqueleto no Title.tsx:

  1. Os imports necessários e a constante do CSS handles:
    //Title.tsx
    import React from 'react'
    import { FormattedMessage } from 'react-intl'
    import { useCssHandles } from 'vtex.css-handles'
    
    const CSS_HANDLES = ['title'] as const 
  2. O componente em si:
    //Title.tsx
    const Title: StorefrontFunctionComponent<TitleProps> = ({title}) => {
      const handles = useCssHandles(CSS_HANDLES)
      const titleText = title || <FormattedMessage id="countdown.title" /> 
    
      return (
        <div className={`${handles.title} t-heading-2 fw3 w-100 c-muted-1 db tc`}>
          { titleText }
        </div> 
      )
    }
  3. A interface, o schema e o export:
    //Title.tsx
    interface TitleProps {
      title: string
    }
      
    Title.schema = {
      title: 'editor.countdown-title.title',
      description: 'editor.countdown-title.description',
      type: 'object',
      properties: {
        title: { 
          title: 'editor.countdown.title.title',
          type: 'string',
          default: null,
        }
      }
    }
      
    export default Title

Alterando o arquivo interfaces.json

Nessa altura, temos dois componentes em uma app: o título e o contador em si. Porém, como fica o arquivo interfaces.json mencionado anteriormente? Agora que possuímos dois componentes, precisamos declará-las separadamente. No início, nossa interface tinha apenas o Countdown. Precisamos adicionar nosso outro componente:

{
  "countdown": {
    "component": "Countdown"
  },
+    "countdown.title": {
+      "component": "Title"
+    }
}

Adicionando internacionalização

Precisamos também adicionar ao Messages as traduções cujas chaves são as strings do schema que incluímos no arquivo Title.tsx logo acima. Como visto na etapa de Messages, precisamos ir à pasta /messages e adicionar em cada um dos arquivos as traduções necessárias. Vamos dar o exemplo para o caso do arquivo en.json:

{
+  "countdown.title": "Countdown",
  "editor.countdown.title": "Countdown",
  "editor.countdown.description": "Countdown component"
}

Adicionando o novo bloco na home da loja

Por fim, para finalmente vermos nossas mudanças, precisamos voltar ao nosso tema para alterá-lo a fim de incluir o novo bloco. Para isso, basta adicionar à home nosso título! Assim como feito para o contador, precisamos adicionar o countdown.title como um bloco no arquivo home.jsonc do store-theme.

//home.jsonc
{
  "store.home": {
    "blocks": [
      "countdown",
+      "countdown.title",
      ...
    ]
  },
  ...
}     

Pronto! Agora vamos ver como deve ser o resultado: image

O que é GraphQL?

Introdução

GraphQL é uma linguagem de query para APIs e uma aplicação server-side que executa queries a partir de um sistema de tipagens que você define para os seus dados. Não é uma tecnologia amarrada a nenhum banco de dados ou Storage.

Um serviço GraphQL funciona a partir de um schema que contém tipos que o desenvolvedor define e campos contidos nesses tipos. O desenvolvedor define que funções vão responder cada um dos tipos. Exemplo: um serviço GraphQL hipotético que diz qual o usuário está logado, assim como os pedidos daquele usuário poderia ser feito dessa forma:

type Query {
  loggedUser: User
}

type User {
  id: ID
  orders: [Order]
}

type Order {
  id: ID
  totalValue: Float
}
const queryLoggedUser = (request) => request.auth.user

const userOrders = (user) => user.getOrders()

GraphQL no VTEX IO

Conforme vimos anteriormente, no VTEX IO, builders são usados para abstrair configurações e complexidades no uso de tecnologias chave. No caso de aplicações back-end, assim como no caso de React, utilizamos TypeScript e oferecemos builders de node e graphql que são utilizados em conjunto.

O builder de graphql é utilizado para criar o schema com seus respectivos tipos e campos. E o de node para criar as funções que resolvem os tipos e campos definidos no schema. Nessa etapa do curso veremos um exemplo de como esses dois builders se conectam.

Definição de Schema

Falando de forma simplificada, um schema GraphQL é composto de dois tipos básicos Query e Mutation. Convencionou-se que queries são utilizadas para realização de fetching de dados. Enquanto as mutations são utilizadas em casos que ocorrem efeitos colaterais ao realizar uma operação, por exemplo alterar algum campo em um banco de dados ou escrever em alguma API.

Para o escopo desse curso, só iremos construir queries, mas o conceito é exatamente o mesmo. O que muda é a semântica da função que resolve o campo.

Vamos adicionar um campo no tipo Query chamado helloWorld que será do tipo String.

type Query {
+  helloWorld: String
}

Ao linkar sua app, você terá acesso a uma URL contendo um link para uma IDE de GraphQL chamada GraphiQL, esse ambiente permite que você teste seu schema de forma rápida, sem precisar desenvolver um cliente em código (pense nele como um Postman GraphQL)

Esse campo precisa ter uma função que irá resolvê-lo na pasta node. É o que iremos fazer agora.

Definição de Resolver

Resolvers são funções responsáveis por "resolver" uma query e devolver o dado solicitado. Vale ressaltar que o retorno de um resolver é uma promise, ou seja, o GraphQL espera a resolução dessa promessa para devolver os resultados obtidos.

Na pasta node, vá na pasta resolvers e crie um novo arquivo helloWorld.ts. Nesse arquivo iremos criar a função que resolverá o campo helloWorld que adicionamos anteriormente no tipo Query.

export const helloWorld = () => 'Hello World'

Agora, se você acessar novamente o GraphiQL e realizar a query abaixo, verá que o GraphiQL retorna o Hello World, conforme o esperado.

{
  helloWorld
}

Atividade

Agora, nos voltaremos à nossa aplicação de countdown com gif. Sua tarefa será criar o campo giphy no schema GraphQL. Esse campo receberá como argumento um term do tipo String que irá em uma atividade futura ser utilizado para realizar uma busca na API do giphy pelo termo passado como argumento.

Feito isso, seu objetivo é observar no GraphiQL o novo campo adicionado.

Definindo um resolver GraphQL

Introdução

Agora que já certificamos que o tipo foi configurado corretamente, vamos definir a função que resolve nossa query. Em GraphQL, podemos definir resolvers para qualquer campo do schema e também para tipos. Para saber mais, leia aqui.

Definiremos a função resolver para o nosso campo gif no serviço node da nossa aplicação. Esta função será, propriamente, a implementação da funcionalidade que estamos criando.

No arquivo node/resolvers/helloWorld.ts você verá que já há um resolver definido para o campo helloWorld, faremos algo semelhante. A assinatura de uma função resolver é a seguinte:

const resolver = (parent, args, context) => {};
Nome Descrição
parent O último objeto que foi resolvido (o parente de tal elemento no grafo do schema). Não é muito útil para queries principais
args O objeto de argumentos que foram passados para aquela query
context Um valor de contexto provido para todo resolver. Aqui você poderá ler informações sobre a requisição, ou usar algum serviço da VTEX

Atividade

Na pasta node/resolvers, crie um arquivo chamado giphy.ts, nele você implementará o resolver do campo gif. A princípio, queremos apenas ver que está tudo funcionando, então nosso resolver irá apenas retornar uma string "it works!". Vamos lá?

// node/resolvers/giphy.ts
export const gif = (_: any,
  __: any,
  ___: Context 
) => { return 'it works!' }

No arquivo node/index.ts, há um objeto exportado com as funções resolvers, adicione um campo chamado gif, este nome deve ser igual ao nome do campo definido no schema.graphql.

// node/resolvers/index.ts
export default new Service<Clients, {}>({
  clients,
  graphql: {
    resolvers: {
      Query: {
        helloWorld,
+       gif 
      },
    },
  },
})

Após isso, salve o arquivo e veja o output do vtex link. Caso seu GraphiQL já esteja aberto, você poderá refazer a query e verificar se o resultado esperado foi obtido.

É importante notar que o tipo de dado retornado pelo seu resolver deve casar com o tipo definido no schema.graphql, senão o GraphQL não vai retornar o valor corretamente. Como nosso campo gif está tipado para retornar uma String e retornamos 'it works!', está tudo bem!

Definindo um client no VTEX IO

Introdução

Já vimos como criamos um resolver GraphQL, e agora iremos continuar com o desenvolvimento da nossa funcionalidade. Comumente, a implementação de uma funcionalidade em nossa app requere a comunicação com outros serviços, sejam externos ou internos (outras apps VTEX), e, para realizar essa comunicação, deveremos criar um client. Um client é uma entidade em nosso serviço encarregado de realizar requisições, e ele é criado reutilizando clients exportados pelo node-vtex-api.

Você pode ver um exemplo de um client criado para se comunicar com um serviço externo na app service-example.

Além de External Clients, como este de exemplo, você pode criar App Clients, para comunicação HTTP com outras apps dentro da conta VTEX IO, App GraphQL Clients, para comunicação através de GraphQL com outras apps também do IO, e Infra Clients para comunicação com serviços de Infra do VTEX IO.

Após a criação de um client, é necessário adicioná-lo na exportação do Service. Após isso, todos os clients padrão e os que você criou estarão disponívels no ctx de cada requisição.

Atividade

Criando o client

  1. Crie um arquivo em node/clients chamado giphy.ts.
  2. A partir do client de exemplo, crie um GiphyClient que se comunica com a API do Giphy na URL https://api.giphy.com/v1/gifs/
  3. O client precisa ter apenas um método chamado translate que aceita um term: string e retornará uma URL de GIF. Este método deverá chamar o endpoint translate da API. OBS.: Use a api_key dp2scGnUcDee5yLRI1qJMTRTAAJey9Tl para testar seu client.
  4. Após criar (e exportar) o client em giphy.ts, em node/clients/index.ts importe Giphy from './giphy' e adicione na classe Clients:
    public get giphy() {
        return this.getOrSet('giphy', Giphy)
    }
    

Alterando nosso resolver

  1. Agora, voltando ao resolver, podemos utilizar ctx.giphy.translate para finalizar a implementação da funcionalidade. Retorne a chamada deste método, informando o termo passado como parâmetro para o resolver. Para isso, precisamos voltar ao arquivo giphy.ts e modificar nossa função, que irá utilizar o método translateGif. Dessa forma, definimos nosso resolver:

    // node/resolvers/giphy.ts
    export const gif = async (
    _: any,
    { term }: { term: string },
    { clients: { giphy }}: Context
    ) => giphy.translateGif(term)

    É possível ver que a estrutura é bem semelhante ao esqueleto de resolver mostrado anteriormente, temos parent, args e, por fim, context. Como já adicionamos nosso resolver ao arquivo node/resolver/index.ts, não precisamos alterá-lo novamente.

  2. Adicione no arquivo manifest.json uma policy para acessar URL externa:

    {
        ...
        "policies": [
            {
            "name": "outbound-access",
            "attrs": {
                "host": "api.giphy.com",
                "path": "*"
            }
            }
        ],
        ...
    }
  3. Teste no GraphiQL sua modificação!

    Para testar o resultado, basta fazer uma query no GraphiQL e colocar no campo de query variables a string que você deseja utilizar para pesquisar um GIF. O resultado esperado é que você receberá uma resposta com o URL de um GIF.

    No nosso exemplo, definimos uma query da seguinte forma:

    query buscaGif ($tema: String) {
    gif(term:$tema)
    }
    

    Além disso, no campo de query variables, definimos tema como cats:

    {
    "tema": "cats"
    }
    

    E o nosso resultado foi:

    {
    "data": {
        "gif": "https://media2.giphy.com/media/3o72EX5QZ9N9d51dqo/giphy.gif?cid=96678fa42d14d68f9c3ebdfaff64b84de51f012598e0a2e9&rid=giphy.gif"
    }
    }
    

Criando um bloco GIF

Introdução

Agora, vamos criar um bloco que irá ser utilizado com um GIF. Porém, como ainda não fizemos a conexão para fazermos uma query e, portanto, uma conexão com o back-end, esse bloco terá um placeholder no lugar do GIF (não se preocupe, adicionaremos o GIF na próxima etapa!).

A maioria dos conceitos abordados nessa etapa já foram vistos anteriormente, como a criação de um novo componente React, a adição de uma interface e a mudança do tema. Vamos lá!

Atividade

  1. Crie o arquivo Gif.tsx na pasta /react; seu formato é muito semelhante ao presente em Title.tsx, mas com as modificações necessárias. Vale ressaltar que o texto a ser exibido é um placeholder, logo, pode ser qualquer coisa dentro de uma div que utilize os estilos já mostrados anteriormente.

    Encorajamos que você tente fazer esse item sozinho, mas se precisar de ajuda, o esqueleto do código está logo abaixo.

    import React from 'react'
    
    import { useCssHandles } from 'vtex.css-handles'
    
    const CSS_HANDLES = ['gif'] as const 
    
    const Gif: StorefrontFunctionComponent<GifProps> = ({ }) => {
        const handles = useCssHandles(CSS_HANDLES)
        return (
            <div className={`${handles.gif} t-heading-2 fw3 w-100 c-muted-1 db tc`}>
                Vou ser um GIF em breve...
            </div>
        )   
    }
    
    interface GifProps {
        
    }
    
    Gif.schema = {
        title: 'editor.countdown-gif.title',
        description: 'editor.countdown-gif.description',
        type: 'object',
        properties: {
        }
    }
    
    export default Gif

    Lembrando que o que está definido no schema é referente às strings internacionalizadas presentes no Site Editor.

  2. Agora que temos o esqueleto do nosso bloco gif, precisamos adicionar a interface equivalente a ele, como fizemos para o contador e para o título. Vá ao arquivo interfaces.json, na pasta /store e adicione a interface equivale ao bloco que você acabou de criar. Não se esqueça de que o campo component deve ter o mesmo nome do componente React em si.

  3. Por fim, precisamos adicionar nosso bloco ao tema, através da home da loja. Para isso, vamos ao store-theme, na pasta /store/blocks/home e, no arquivo home.jsonc, adicionamos o bloco gif.

O resultado esperado nesse passo é:

image

Conectando back-end e front-end

Introdução

Já temos nossa componente React que irá renderizar GIFs. O que precisamos fazer agora é criar uma query para ser executada pelo resolver que criamos nos steps nos anteriores. Para realizar queries GraphQL em React, utilizamos o apollo-client, uma biblioteca de gerenciamento de estado que facilita a integração de uma API GraphQL com a aplicação front-end.

O time do apollo-graphql disponibiliza uma integração nativa com React, por meio de hooks. Dessa forma, realizar uma query significa escrever um hook que não só realizará as queries e fará o fetch dos dados, mas também proverá cache e atualização do estado do UI. Essa integração, chamada react-apollo já está declarada no package.json.

Query de Gifs

  1. Crie o arquivo Gif.tsx na pasta /react; seu formato é muito semelhante ao presente em Title.tsx, mas com as modificações necessárias. Vale ressaltar que o texto a ser exibido é um placeholder, logo, pode ser qualquer coisa dentro de uma div que utilize os estilos já mostrados anteriormente.

  2. Crie uma pasta react/queries e nela adicione um arquivo gifs.gql que irá conter a query a ser feita. Em particular, essa query irá receber um termo, que será a palavra-chave a ser utilizada para procurar GIFs no Giphy. Ela chamará o resolver gif, implementado e testado no GraphiQL no passo anterior.

    query getGif ($term: String) {
        gif(term:$term)
    }
    
  3. Defina a prop term na interface GifProps e a utilize como prop do componente React Gif. Não se esqueça de atribuir um valor padrão para ela.

  4. Agora, precisamos importar o método useQuery e utilizá-lo para fazer a query que irá nos retornar o URL de um GIF. Além disso, também precisamos importar nossa query em si, definida anteriormente, que se encontra no arquivo gifs.gql.

    // react/Gif.tsx
    import React from 'react'
    +import { useQuery } from 'react-apollo'
    
    import { useCssHandles } from 'vtex.css-handles'
    
    +import getGif from './queries/gifs.gql'
  5. Em um primeiro momento, vamos verificar se nossa query está funcionando através de console.log(data), que deve nos mostrar um objeto gif com um par de chave-valor, onde a chave é url e o valor é a URL em si.

  6. Para vermos nosso GIF na home da loja, precisamos adicionar uma imagem que possua como source src o valor desse objeto, ou seja, data.gif.

    // react/Gif.tsx
    const Gif: StorefrontFunctionComponent<GifProps> = ({ term = 'VTEX' }) => {
        const handles = useCssHandles(CSS_HANDLES)
        const { data, loading, error } = useQuery(query, {
          variables: { term }
        })
        return (
            <div className={`${handles.gif} t-heading-2 fw3 w-100 c-muted-1 db tc`}>
                <img src={data.gif} />
            </div>
        )
    }
  7. Por fim, vamos alterar nosso schema para adicionarmos o campo de term no Site Editor e, como feito anteriormente na etapa de internacionalização, defina as strings necessárias nos arquivos dentro da pasta messages/

    // react/Gif.tsx
    Gif.schema = {
        title: 'admin/gif.title',
        description: 'admin/gif.description',
        type: 'object',
        properties: {
            term: {
                title: 'admin/gif.term.title',
                description: 'admin/gif.term.description',
                type: 'string',
                default: null,
            },
        },
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment