Antes de começar, é necessário relembrar alguns conceitos importantes para uma maior compreensão do fluxo lógico ao desenvolver uma app.
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.
Identifica o nome da aplicação. Não deve ter caracteres especiais - exceto -
- ou caracteres maiúsculos.
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.
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
.
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.
Uma app pode depender de outras aplicações. Esse campo lista todas as dependências necessárias para o correto funcionamento da app.
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"
}
Para desenvolver um bloco de frente de loja, similar aos que oferecemos nativamente no Store Framework, utilizamos a biblioteca de desenvolvimento de UIs react
.
É 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.
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.
Quando você estiver desenvolvendo seu próprio bloco, você pode começar por nosso template de react.
-
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
-
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
-
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 nomanifest.json
.vtex link
-
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", ... }, ... }
-
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:
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.
-
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.
-
Na interface definida no
Countdown.tsx
, adicione uma prop chamadatargetDate
, 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 arquivoCountdown.tsx
, mostrada anteriormente. Assim, adicione uma linha que defina uma prop chamadatargetDate
, do tipo string.// react/Countdown.tsx interface CountdownProps { + targetDate: string }
-
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 noh1
. 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> ) }
-
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 objetoproperties
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.
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
;
É 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'
})
-
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'
-
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. -
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()
-
Utilize a função
tick
e a constanteDEFAULT_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> ) }
-
Altere o
h1
para que ele exiba o contador que criamos. Para isso, precisamos utilizar o estado atualtimeRemaining
: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 emhours
,minutes
eseconds
.
Assim, com essas alterações, veremos a atualização em tempo real do contador! O resultado na home é esse aqui:
E veja o contador funcionando:
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.
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.
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.
-
Importe o
useCssHandles
. Para isso, volte aoCountdown.tsx
e faça o import:// react/Countdown.tsx import { useCssHandles } from 'vtex.css-handles'
-
Além disso, defina a constante do estilo que iremos puxar do handles. Neste caso, o
countdown
:// react/Countdown.tsx const CSS_HANDLES = [ 'countdown' ]
-
Utilize o
useCssHandles
no componenteCountdown
para "pegar" o estilo que necessário docountdown
e, além disso, troque oFragment
por uma tag dediv
:// 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> ) }
-
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 oFragment
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?
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.
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.
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
-
Adicione a biblioteca usando
yarn add react-intl
na pasta react -
No código do seu componente
Countdown.tsx
importe o FormattedMessage+ import { FormattedMessage } from 'react-intl'
-
Adicione uma constante que será o seu título:
const titleText = title || <FormattedMessage id="countdown.title"/>
-
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:
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?
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"
}
}
Nessa atividade, vamos separar o título e adicionar à nossa loja embaixo do contador. Vamos lá?
No arquivo Countdown.tsx
, é preciso remover algumas linhas de código.
- 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']
- 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> ) }
- 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
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
:
- 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
- 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> ) }
- 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
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"
+ }
}
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"
}
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:
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()
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.
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.
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
}
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.
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 |
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!
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.
- Crie um arquivo em
node/clients
chamadogiphy.ts
. - 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/ - O client precisa ter apenas um método chamado
translate
que aceita umterm: string
e retornará uma URL de GIF. Este método deverá chamar o endpoint translate da API. OBS.: Use aapi_key
dp2scGnUcDee5yLRI1qJMTRTAAJey9Tl
para testar seu client. - Após criar (e exportar) o client em
giphy.ts
, emnode/clients/index.ts
importeGiphy from './giphy'
e adicione na classeClients
:public get giphy() { return this.getOrSet('giphy', Giphy) }
-
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 arquivogiphy.ts
e modificar nossa função, que irá utilizar o métodotranslateGif
. 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 arquivonode/resolver/index.ts
, não precisamos alterá-lo novamente. -
Adicione no arquivo
manifest.json
uma policy para acessar URL externa:{ ... "policies": [ { "name": "outbound-access", "attrs": { "host": "api.giphy.com", "path": "*" } } ], ... }
-
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" } }
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á!
-
Crie o arquivo
Gif.tsx
na pasta/react
; seu formato é muito semelhante ao presente emTitle.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 umadiv
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.
-
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 campocomponent
deve ter o mesmo nome do componente React em si. -
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 arquivohome.jsonc
, adicionamos o blocogif
.
O resultado esperado nesse passo é:
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
.
-
Crie o arquivo
Gif.tsx
na pasta/react
; seu formato é muito semelhante ao presente emTitle.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 umadiv
que utilize os estilos já mostrados anteriormente. -
Crie uma pasta
react/queries
e nela adicione um arquivogifs.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 resolvergif
, implementado e testado no GraphiQL no passo anterior.query getGif ($term: String) { gif(term:$term) }
-
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. -
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 arquivogifs.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'
-
Em um primeiro momento, vamos verificar se nossa query está funcionando através de
console.log(data)
, que deve nos mostrar um objetogif
com um par de chave-valor, onde a chave éurl
e o valor é a URL em si. -
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> ) }
-
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 pastamessages/
// 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, }, }, }