JavaScript no WordPress é o inferno… e aqui está o porquê
Publicados: 2022-05-05A pilha de desenvolvimento do WordPress mudou muito nos últimos anos. Desde a chegada de Gutenberg, o papel do JavaScript em nosso CMS favorito é mais importante do que nunca. Neste blog, já falamos longamente sobre as vantagens que isso traz para os desenvolvedores (para citar alguns exemplos, falamos sobre extensões para Gutenberg, conselhos sobre TypeScript e React, ferramentas de desenvolvimento, exemplos de plugins e muito mais), mas cada história tem seu lado sombrio ... e é sobre isso que vamos falar aqui hoje.
No post de hoje, vou compartilhar com você os 3 principais problemas que você, desenvolvedor de plugins WordPress, pode enfrentar no futuro. E o engraçado é que cada um deles tem um culpado diferente: o próprio WordPress, outros desenvolvedores ou você mesmo. Então aqui vamos nós: as dores de cabeça JavaScript mais comuns que você pode encontrar e o que você pode fazer para evitá-las/corrigir.
Plugins WPO nº 1 que quebrarão seu plug-in e seu site
Vamos começar com o problema que tem sido a fonte de muitos tickets aqui no Nelio: plugins WPO.
Tenho certeza de que você leu muitos artigos e postagens de blog que descrevem a importância de ter um site leve que carregue rapidamente. Quer dizer, nós também escrevemos em várias ocasiões sobre isso! Entre as dicas que eles costumam dar, você encontrará coisas como mudar para um provedor de hospedagem melhor, usar plugins de cache e CDNs, manter seu servidor e WordPress atualizados ou (e aqui vem o primeiro problema) instalar um plugin WPO. Alguns exemplos deste último incluem:
- W3 Total Cache com mais de 1 milhão de instalações
- SiteGround Optimizer com mais de 1 milhão de instalações também (uma das quais, a propósito, usamos em nosso site)
- WordPress WPO Tweaks & Otimizações pelo nosso bom amigo Fernando Tellado
Esses plugins prometem acelerar seu site por meio de uma série de otimizações sensatas que, em geral, “qualquer site WordPress pode se beneficiar”. Essas otimizações incluem:
- Desenfileiramento de scripts desnecessários no frontend, como emojis ou Dashicons
- Cache de páginas e consultas de banco de dados
- Reduzindo a quantidade de informações incluídas no cabeçalho
- Combinando e minificando scripts JavaScript e estilos CSS
- Reduzindo HTML
- Removendo o argumento de consulta de versão dos URLs de seus recursos estáticos
- Adiando scripts JavaScript e/ou carregando-os de forma assíncrona
- etc
Como eu estava dizendo, esses tipos de otimizações podem ser, em geral, benéficos. Mas em nossa experiência, todas as otimizações de JS em um site WordPress tendem a resultar em mais problemas , tornando inúteis suas supostas melhorias. Exemplos reais que eu vi ao longo dos anos são:
- Combinando roteiros. Quanto menos scripts seu navegador precisar solicitar, melhor. É por isso que faz sentido combinar todos os scripts em um. No entanto, isso pode ser problemático. Em geral, se um script JavaScript trava, sua execução termina e o erro é relatado no console do seu navegador. Mas apenas a execução desse script é interrompida; seus outros scripts serão executados normalmente. Mas se você combinar todos eles... bem, assim que um script falhar, os outros scripts (incluindo talvez o seu) não serão executados, e seus usuários pensarão que é o seu plugin que não está funcionando como esperado.
- Minificando scripts. Eu vi alguns processos de minificação que, acredite ou não, quebravam expressões regulares e resultavam em scripts JS com erros de sintaxe. Claro, já faz um tempo desde a última vez que encontrei este, mas… :-/
- Argumentos da consulta. Quando você enfileira um script no WordPress, você pode fazer isso com seu número de versão (que, a propósito, provavelmente foi gerado automaticamente por
@wordpress/scripts
). Os números de versão são extremamente úteis: se você atualizar seu plugin e seu script for alterado, esse novo número de versão garantirá que todos os visitantes vejam uma URL diferente e, portanto, seus navegadores solicitarão a nova versão. Infelizmente, se um plug-in WPO remover a string de consulta, seus visitantes podem não perceber que o script foi alterado e estarão usando uma cópia em cache desse script… o que pode ou não resultar em consequências não intencionais. Droga!
Um desastre completo, não é? Mas espere até ouvir o próximo:
Adiando scripts
Na Nelio, implementamos um plug-in de teste A/B para rastrear seus visitantes e descobrir qual design e conteúdo obtém mais conversões. Como você pode imaginar, nosso script de rastreamento é semelhante a este:
window.NelioABTesting = window.NelioABTesting || {}; window.NelioABTesting.init = ( settings ) => { // Add event listeners to track visitor events... console.log( settings ); };
Ou seja, ele expõe uma função init
que temos que chamar para informar ao script sobre os testes que estão sendo executados no momento. Para chamar esse método, enfileiramos um script embutido em PHP da seguinte forma:
function nab_enqueue_tracking_script() { wp_enqueue_script( 'nab-tracking', ... ); wp_add_inline_script( 'nab-tracking', sprintf( 'NelioABTesting.init( %s );', wp_json_encode( nab_get_tracking_settings() ) ) ); } add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );
que resulta nas seguintes tags HTML:
<head> ... <script type="text/javascript" src="https://.../dist/tracking.js" ></script> <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ...
Mas o que acontece quando seu plugin WPO adiciona o atributo defer
ao nosso script?
<head> ... <script defer <!-- This delays its loading... --> type="text/javascript" src="https://.../dist/tracking.js" ></script> <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ...
Bem, o script agora está adiado… o que significa que o trecho anterior é equivalente a este:
<head> ... <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ... <script type="text/javascript" src="https://.../dist/tracking.js" ></script> </body> </html>
e, como resultado, nab-tracking-js
não é mais carregado quando deveria e, portanto, o script embutido que vem depois dele e depende dele simplesmente falhará: nab-tracking-js-after
usa NelioABTesting.init
que , graças à diretiva defer
, ainda não está disponível. Horrível!
Solução
A solução mais eficaz é clara: diga aos seus usuários para desativar a otimização de script e encerrar o dia. Afinal, o gerenciamento de dependências em JavaScript é, em geral, extremamente complicado (especialmente se usarmos diretivas defer
e async
), e o WordPress não é exceção. Basta dar uma olhada nesta discussão de 12 anos sobre o tema!
Mas se isso não for viável (e eu sei que não é), recomendo que você faça a mesma coisa que fizemos: se livre do método init
e inverta as responsabilidades de seus scripts regulares e embutidos. Ou seja, adicione um script embutido antes do script normal e use-o para definir uma variável global com as configurações obrigatórias:
function nab_enqueue_tracking_script() { wp_enqueue_script( 'nab-tracking', ... ); wp_add_inline_script( 'nab-tracking', sprintf( 'NelioABTestingSettings = %s;', wp_json_encode( nab_get_tracking_settings() ) ), 'before' ); } add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );
para que o HTML resultante fique assim:
<head> ... <script type="text/javascript" > NelioABTestingSettings = {"experiments":[...],...}; </script> <script type="text/javascript" src="https://.../dist/tracking.js" ></script> ... </head> <body> ...
e, portanto, não importa se a execução do script externo está atrasada ou não - ele sempre aparecerá após o script embutido, satisfazendo assim a dependência entre eles.
Finalmente, se você quiser ter certeza de que ninguém vai modificar suas configurações, declare a variável como const
e congele seu valor com Object.freeze
:
... sprintf( 'const NelioABTestingSettings = Object.freeze( %s );', wp_json_encode( nab_get_tracking_settings() ) ), ...
que é suportado por todos os navegadores modernos.
#2 Dependências no WordPress que podem ou não funcionar…
O gerenciamento de dependências também pode ser problemático no WordPress, especialmente quando se fala sobre os scripts internos do WordPress. Deixe-me explicar.
Imagine, por exemplo, que estamos criando uma pequena extensão para o Gutenberg, conforme explicamos aqui. O código-fonte do nosso plugin provavelmente terá algumas instruções de import
como estas:
import { RichTextToolbarButton } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { registerFormatType } from '@wordpress/rich-text'; // ...
Quando este código-fonte JS é transpilado, o Webpack (ou a ferramenta que você usa) empacotará todas as dependências e seu próprio código-fonte em um único arquivo JS. Este é o arquivo que você enfileirará mais tarde do WordPress para que tudo funcione como você espera.
Se você usou @wordpress/scripts
para criar tal arquivo, algumas das dependências não serão incluídas no arquivo de saída, porque o processo interno assume que os pacotes estarão disponíveis no escopo global. Isso significa que as importações anteriores serão transpiladas para algo semelhante a isto:
const { RichTextToolbarButton } = window.wp.blockEditor; const { __ } = window.wp.i18n; const { registerFormatType } = window.wp.richText; // ...
Para garantir que você não perca nenhuma das dependências do seu script, @wordpress/scripts
não apenas transpilará seu código JS, mas também gerará um arquivo PHP com suas dependências do WordPress:
<?php return array( 'dependencies' => array('wp-block-editor','wp-i18n','wp-rich-text'), 'version' => 'a12850ccaf6588b1e10968124fa4aba3', );
Bem legal, hein? Então, qual é o problema? Bem, esses pacotes do WordPress estão em desenvolvimento contínuo e mudam com bastante frequência, adicionando novos recursos e melhorias. Portanto, se você desenvolver seu plugin usando a versão mais recente do WordPress, poderá acabar usando inadvertidamente funções ou recursos que estão disponíveis nessa versão mais recente (e, portanto, tudo funciona como deveria), mas não nas versões “mais antigas” do WordPress…
Como você sabe?
Solução
Meu conselho aqui é muito simples: desenvolva seus plugins usando a versão mais recente do WordPress, mas teste seus lançamentos em versões mais antigas. Em particular, sugiro que você teste seu plug-in com, pelo menos, a versão mínima do WordPress que seu plug-in deve suportar. Uma versão mínima você encontrará no readme.txt
do seu plugin:
=== Nelio Content === ... Requires PHP: 7.0 Requires at least: 5.4 Tested up to: 5.9 ...
Mudar de uma versão do WordPress para outra é tão fácil quanto executar o seguinte comando WP CLI:
wp core update --version=5.4 --force
#3 As funções de seta são mais complicadas do que você pensa
Finalmente, deixe-me compartilhar um dos problemas mais recentes que encontrei apenas alguns dias atrás e que me deixou louco. Em poucas palavras, tínhamos um arquivo JavaScript semelhante a este:
import domReady from '@wordpress/dom-ready'; domReady( () => [ ...document.querySelectorAll( '.nelio-forms-form' ) ] .forEach( initForm ) ); // Helpers // ------- const initForm = ( form ) => { ... } // ...
que inicializa seu Nelio Forms no front-end. O roteiro é bem simples, não é? Ele define uma função anônima que é chamada quando o DOM está pronto. Esta função usa uma função auxiliar (seta) chamada initForm
. Bem, como se vê, um exemplo tão simples pode falhar! Mas apenas sob algumas circunstâncias específicas (ou seja, se o script foi “otimizado” por um plugin WPO usando o atributo defer
).
Veja como o JS executa o script anterior:
- A função anônima dentro
domReady
é definida -
domReady
funciona - Se o DOM ainda não estiver pronto (e geralmente não está quando um script é carregado),
domReady
não executa a função de retorno de chamada. Em vez disso, ele simplesmente o acompanha para que possa chamá-lo mais tarde - JavaScript continua analisando o arquivo e carrega a função
initForm
- Uma vez que o DOM está pronto, a função de retorno de chamada é finalmente chamada
Agora, e se, no momento em que chegarmos à terceira etapa, o DOM estiver pronto e, portanto, o domReady
chamar a função anônima diretamente? Bem, nesse caso, o script irá acionar um erro indefinido, pois initForm
ainda é undefined
.
Na verdade, o mais curioso de tudo isso é que essas duas soluções são equivalentes:
domReady( aux ); const aux = () => {};
domReady( () => aux() ); const aux = () => {}
o linter JavaScript só lançará um erro no primeiro, mas não no mais recente.
Solução
Existem duas soluções possíveis: ou você define a função auxiliar usando a palavra-chave function
e esquece a função de seta, ou você move a instrução domReady
no final, depois que todas as funções auxiliares foram definidas:
domReady( aux ); function aux() { // ... }
const aux = () => { // ... }; domReady( aux );
Se você está se perguntando por que a primeira solução funciona se é aparentemente equivalente à original que tínhamos, é tudo sobre como funciona o içamento de JavaScript. Em resumo, em JavaScript você pode usar uma função (definida com function
) antes de sua definição, mas não pode fazer o mesmo com variáveis e constantes (e, portanto, funções de seta).
Em suma
Há um grande número de coisas que podem dar errado em JavaScript. Felizmente, todos eles têm uma solução, especialmente se prestarmos atenção ao que fazemos. Espero que você tenha aprendido algo novo hoje e confio que, graças aos erros e erros que cometi no passado, você poderá evitar sofrê-los em sua própria carne no futuro.
Imagem em destaque por Ian Stauffer no Unsplash.