← Voltar ao blog

Escrevendo componentes Alpine.js no Hyvä: padrões que escalam

Rodrigo Biazzi

Em uma loja Hyvä, a maior parte da interatividade do frontend é escrita com Alpine.js, direto nos templates PHP. Para quem vem do Luma, isso parece estranho no começo. Não tem config do RequireJS, não tem widget factory, não tem binding do Knockout. Você escreve uma pequena função ao lado do markup e o Alpine faz a ligação. Este artigo mostra como estruturo componentes Alpine em projetos Hyvä, como eles recebem dados do PHP, como conversam entre si e com o customer data do Magento, e os erros que os tornam difíceis de manter.

A forma básica de um componente Alpine no Hyvä

Um componente é um bloco de markup com um atributo x-data. No Hyvä, a convenção é definir o componente como uma função nomeada em uma tag script dentro do mesmo template, e referenciá-la no x-data. Isso mantém a lógica legível e evita expressões JavaScript longas dentro de atributos HTML.

<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
$limit = 3;
?>
<div x-data="initStockNotice()" class="my-4">
    <p x-show="!open" class="text-sm">
        <?= $escaper->escapeHtml(__('Only a few left in stock.')) ?>
        <button type="button" class="underline" @click="open = true">
            <?= $escaper->escapeHtml(__('Details')) ?>
        </button>
    </p>
    <p x-show="open" x-cloak class="text-sm">
        <?= $escaper->escapeHtml(__('Orders with more than %1 units may ship separately.', $limit)) ?>
    </p>
</div>
<script>
    function initStockNotice() {
        return {
            open: false
        }
    }
</script>

Alguns pontos que importam aqui:

  • Toda saída é escapada com $escaper, igual a qualquer template do Magento. O Alpine não muda a sua responsabilidade com XSS.
  • O x-cloak esconde o elemento até o Alpine inicializar. O Hyvä já inclui a regra de CSS para isso.
  • O nome da função é global. Se o mesmo template pode ser renderizado mais de uma vez na página, a função é definida mais de uma vez, o que é inofensivo, mas dois templates diferentes não podem usar o mesmo nome para componentes diferentes. Coloque nos nomes um prefixo específico do módulo.

Passando dados do PHP para o Alpine

A maioria dos componentes reais precisa de dados do servidor: um ID de produto, um valor de configuração, um preço, uma URL. Há duas formas limpas de passar.

Argumentos para a função de init

Para poucos valores escalares, passe como argumentos. Codifique em JSON para que tipos e aspas fiquem corretos.

<?php
/** @var \Magento\Catalog\Block\Product\View $block */
/** @var \Magento\Framework\Escaper $escaper */
$product = $block->getProduct();
$config = [
    'productId' => (int) $product->getId(),
    'endpoint'  => $block->getUrl('acme_stock/ajax/check'),
];
?>
<div x-data="acmeStockCheck(<?= $escaper->escapeHtmlAttr(json_encode($config)) ?>)">
    <button type="button" @click="check()" :disabled="loading">
        <?= $escaper->escapeHtml(__('Check availability')) ?>
    </button>
    <p x-text="message"></p>
</div>
<script>
    function acmeStockCheck(config) {
        return {
            loading: false,
            message: '',
            check() {
                this.loading = true;
                const url = config.endpoint + '?product=' + encodeURIComponent(config.productId);
                fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
                    .then(response => response.json())
                    .then(data => { this.message = data.message; })
                    .catch(() => { this.message = ''; })
                    .finally(() => { this.loading = false; });
            }
        }
    }
</script>

O JSON vai dentro de um atributo, então é escapado com escapeHtmlAttr. Não cole valores PHP crus em strings JavaScript.

View models para qualquer coisa com lógica

Se os dados precisam de uma chamada a um repository ou de uma consulta de configuração, coloque em um view model e mantenha o template enxuto. Os templates do Hyvä têm uma variável $viewModels que entrega qualquer classe de view model pelo nome, então você não precisa declarar cada uma como argumento de block no layout XML.

<?php
/** @var \Hyva\Theme\Model\ViewModelRegistry $viewModels */
/** @var \Acme\Stock\ViewModel\StockNotice $stockNotice */
$stockNotice = $viewModels->require(\Acme\Stock\ViewModel\StockNotice::class);
?>

O view model é uma classe normal que implementa Magento\Framework\View\Element\Block\ArgumentInterface, com as dependências injetadas pelo constructor. É o mesmo padrão que você usaria no Luma. A diferença é só como você obtém a instância no template.

Customer data e conteúdo privado

O full page cache faz o HTML ser o mesmo para todo visitante. A contagem do carrinho, o nome do cliente e dados parecidos vêm das customer data sections carregadas por JavaScript depois que a página renderiza. No Luma, componentes Knockout se inscrevem nelas pelo módulo Magento_Customer/js/customer-data. No Hyvä não há RequireJS, então os dados chegam como um evento do navegador.

Quando as sections são carregadas, o Hyvä dispara um evento private-content-loaded no window com os dados da section em event.detail.data. Um componente escuta esse evento com a sintaxe de eventos do Alpine.

<div x-data="acmeCartBadge()"
     @private-content-loaded.window="receive($event.detail.data)">
    <span x-show="count > 0" x-text="count"></span>
</div>
<script>
    function acmeCartBadge() {
        return {
            count: 0,
            receive(data) {
                if (data.cart && data.cart.summary_count) {
                    this.count = data.cart.summary_count;
                }
            }
        }
    }
</script>

Se o seu componente altera algo no servidor que afeta uma section, por exemplo adicionar ao carrinho por um controller próprio, peça ao Hyvä para recarregar os dados da section depois.

window.dispatchEvent(new CustomEvent('reload-customer-section-data'));

As sections em si são definidas do mesmo jeito que no Luma, em etc/frontend/sections.xml e com uma classe de origem da section. O lado do servidor não muda. Para entender como as sections e a invalidação funcionam, veja customer data sections do Magento 2.

Form keys e requests POST

Controllers do Magento que aceitam POST checam o form key. No Hyvä você o obtém do objeto global hyva em vez de ler um cookie pelo jQuery.

const body = new URLSearchParams();
body.append('form_key', hyva.getFormKey());
body.append('product', productId);

fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body
});

Para formulários HTML clássicos renderizados no template, você pode continuar usando $block->getBlockHtml('formkey') para imprimir o input hidden.

Comunicação entre componentes

Os componentes de uma página Hyvä são independentes. Quando um precisa avisar outro de que algo aconteceu, use eventos do navegador. Dispare um evento custom em um componente e escute com .window no outro.

<div x-data="{ qty: 1 }">
    <input type="number" min="1" x-model.number="qty"
           @change="$dispatch('acme-qty-changed', { qty: qty })">
</div>

<div x-data="{ qty: 1 }" @acme-qty-changed.window="qty = $event.detail.qty">
    <span x-text="qty"></span>
</div>

Coloque prefixo nos nomes dos eventos. Nomes genéricos como update ou change um dia vão colidir com outra coisa na página. Para estado que muitos componentes compartilham, os stores do Alpine são uma opção, mas na maioria das páginas do Magento os eventos bastam e são mais fáceis de rastrear.

Content Security Policy

O Magento 2.4.7 e posteriores aplicam regras de CSP mais rígidas nas páginas de checkout, e lojas com uma policy estrita não podem depender de scripts inline sem nonces ou hashes. As versões recentes do Hyvä oferecem um jeito de registrar scripts inline para que recebam o nonce correto, e um jeito compatível com CSP de escrever componentes que evita avaliar expressões vindas de atributos. Se a sua loja precisa de uma CSP estrita, siga desde o começo a seção de CSP da documentação do Hyvä para a sua versão. Adaptar isso depois em dezenas de componentes dá muito mais trabalho.

Mantendo os componentes sustentáveis

  • Mantenha markup e lógica no mesmo template enquanto o componente for pequeno. Mova a lógica para um template separado, incluído pelo block, quando passar do que cabe em uma tela.
  • Não ponha regras de negócio em JavaScript. Preços, regras de estoque e elegibilidade pertencem ao PHP. O Alpine deve renderizar estado e reagir ao usuário.
  • Evite expressões longas em atributos. @click="open = !open" está bom. Um ternário com três condições pertence a um método.
  • Use x-show para o que alterna com frequência e x-if dentro de uma tag template para markup pesado que raramente aparece.
  • Adie trabalho caro até o usuário precisar. Use x-intersect se o plugin Intersect estiver carregado no seu tema, ou inicialize na primeira interação. As páginas Hyvä são rápidas porque pouco JavaScript roda no carregamento, então mantenha assim.

Debugging

A maioria dos bugs de Alpine no Hyvä é uma de três coisas: um erro de JavaScript na função de init (olhe o console primeiro), uma colisão de nome de função entre dois templates, ou dados que não foram escapados corretamente e quebraram o atributo. Veja o código-fonte HTML renderizado, não só o inspetor do DOM, para ver exatamente o que o PHP produziu. A extensão Alpine devtools do navegador também ajuda a inspecionar o estado do componente em uma página ao vivo.

Se um componente funciona em um carregamento novo da página mas não depois que o cache esquenta, o problema quase sempre é dado privado renderizado em PHP. Mova para uma customer data section e deixe o componente ler pelo evento.

FAQ

Posso usar jQuery dentro de um componente Alpine no Hyvä?

Dá para carregar, mas não deveria. A performance do Hyvä vem de não enviar jQuery, RequireJS e Knockout. As APIs nativas de fetch e DOM cobrem o que os componentes da storefront do Magento precisam.

Como os componentes do Hyvä obtêm os dados do carrinho e do cliente?

O Hyvä dispara um evento private-content-loaded no window com as customer data sections. Os componentes escutam com @private-content-loaded.window e leem event.detail.data.

Onde devo colocar o JavaScript de um componente?

Em uma tag script no mesmo template do markup, como uma função nomeada referenciada pelo x-data. Componentes grandes podem ser divididos em um template separado, renderizado pelo mesmo block.

Preciso de uma etapa de build para componentes Alpine?

Não. Componentes Alpine são JavaScript puro nos templates. Só o Tailwind CSS precisa de build quando você adiciona classes novas.

Atendo como freelancer em Magento e em frontend Hyvä. Se precisar de ajuda com isso, fale comigo.

Artigos relacionados