Mixins do RequireJS no Magento 2: estenda o JS do core sem override
Quando uma loja Magento 2 precisa de uma mudança de JavaScript no core ou em código de terceiros, a correção tentadora é copiar o arquivo para o tema e editar. Funciona até o próximo upgrade, quando o original muda e a sua cópia continua em silêncio com o código antigo. Mixins do RequireJS resolvem isso do mesmo jeito que plugins resolvem em PHP: você envolve ou estende o módulo original e muda só a parte que importa.
Este artigo mostra como os mixins funcionam no Magento 2.4.8, os três padrões que você precisa para os três tipos de módulo JavaScript do Luma e como conferir se um mixin está realmente carregado.
Como os mixins funcionam
O Magento adiciona um pequeno plugin do RequireJS, o mixins, que fica entre um módulo e quem o requisita. Quando um módulo tem mixins configurados, o RequireJS carrega o original e depois passa o retorno dele para cada mixin em sequência. O que o último mixin retornar é o que o resto do código recebe.
Um mixin é, portanto, só uma função que recebe o export do módulo original e devolve um modificado. O que você retorna depende do que o módulo original retorna, e é aí que acontece a maioria dos erros.
Declarando um mixin
Mixins são declarados no requirejs-config.js, num módulo (view/frontend/requirejs-config.js) ou num tema (web/requirejs-config.js, ou dentro de uma pasta de módulo no tema).
var config = {
config: {
mixins: {
'Magento_Checkout/js/view/shipping': {
'Vendor_Checkout/js/view/shipping-mixin': true
},
'Magento_Catalog/js/catalog-add-to-cart': {
'Vendor_Catalog/js/catalog-add-to-cart-mixin': true
},
'Magento_Checkout/js/action/place-order': {
'Vendor_Checkout/js/action/place-order-mixin': true
}
}
}
};
A chave é o caminho do módulo original, exatamente como o outro código o requisita. O valor é um mapa dos caminhos dos seus mixins para true. Definir um mixin como false numa config carregada depois o desativa, o que é um jeito limpo de desligar o mixin de um módulo de terceiros sem editar código do vendor.
Todos os arquivos requirejs-config.js são combinados em um arquivo por tema e locale durante o static content deploy. No modo production, nada muda até você fazer o deploy de static content de novo.
Padrão 1: UI components
Um módulo de UI component retorna um construtor criado com Component.extend(). Seu mixin recebe esse construtor e devolve um estendido.
define([], function () {
'use strict';
return function (Shipping) {
return Shipping.extend({
validateShippingInformation: function () {
var isValid = this._super();
if (isValid && !this.isDeliveryNoteValid()) {
return false;
}
return isValid;
},
isDeliveryNoteValid: function () {
return true;
}
});
};
});
O this._super() chama o método original, incluindo as mudanças de mixins que rodaram antes do seu. Sempre chame, a não ser que tenha um motivo para substituir o método por completo, e se tiver, escreva esse motivo na mensagem do commit. Substituir um método quebra todos os outros mixins que o envolvem.
Para saber mais sobre como esses componentes são construídos, veja o artigo UI components e noções básicas de Knockout.
Padrão 2: widgets jQuery
Muitos módulos do Luma são widgets do jQuery UI declarados com $.widget(). O módulo retorna o construtor do widget. Para estendê-lo, você redeclara o widget usando o original como base e retorna o novo.
define([
'jquery'
], function ($) {
'use strict';
return function (widget) {
$.widget('mage.catalogAddToCart', widget, {
ajaxSubmit: function (form) {
this.element.trigger('vendor:beforeAddToCart');
return this._super(form);
}
});
return $.mage.catalogAddToCart;
};
});
O nome do widget precisa ser exatamente o do original. Se você o registrar com um nome novo, o código que chama catalogAddToCart continua usando o original e o seu mixin não faz nada. Abra o arquivo original e leia o nome na chamada $.widget antes de escrever o mixin.
Padrão 3: módulos que retornam uma função
Actions do checkout, como place-order ou set-shipping-information, retornam uma função simples. Não dá para fazer extend numa função, então você a envolve com mage/utils/wrapper.
define([
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function (wrapper, quote) {
'use strict';
return function (placeOrderAction) {
return wrapper.wrap(placeOrderAction, function (originalAction, paymentData, messageContainer) {
paymentData.extension_attributes = paymentData.extension_attributes || {};
paymentData.extension_attributes.delivery_note = quote.deliveryNote || '';
return originalAction(paymentData, messageContainer);
});
};
});
O wrapper passa a função original como primeiro argumento, seguida dos argumentos reais. Retorne o resultado da chamada original, porque quem chama costuma esperar uma promise. Esquecer o return é um jeito clássico de quebrar o botão de finalizar pedido sem nenhum erro no console.
Enviar um valor extra junto com os dados de pagamento também exige uma parte no servidor, normalmente um extension attribute e um plugin. O artigo sobre campo customizado no checkout cobre isso de ponta a ponta.
Módulos que retornam um objeto
Alguns módulos retornam um objeto simples com métodos, como vários models do checkout. Nesse caso, altere o método no objeto e retorne o mesmo objeto:
define([
'mage/utils/wrapper'
], function (wrapper) {
'use strict';
return function (target) {
target.someMethod = wrapper.wrap(target.someMethod, function (original) {
var args = Array.prototype.slice.call(arguments, 1);
return original.apply(this, args);
});
return target;
};
});
Como o objeto é compartilhado, a mudança vale para todo módulo que o usa, que costuma ser o que você quer.
Mixin, map ou copiar o arquivo
- Mixin: você muda um ou dois métodos e mantém todo o resto. Esse deve ser o seu padrão.
- map no
requirejs-config.js: você substitui o módulo inteiro por um arquivo seu para todos que o chamam. Use quando precisar reescrever a maior parte do módulo. Você assume a manutenção de tudo. - Override no tema do mesmo caminho: mesmo efeito do map para aquele tema, e o mesmo custo de manutenção.
É o mesmo raciocínio de escolher entre plugin e preference em PHP. O artigo plugins, observers e preferences explica esse lado em detalhe.
Mantendo os mixins sustentáveis
Mixins são baratos de escrever, então as lojas tendem a juntar muitos. Alguns hábitos seguram isso.
- Um arquivo de mixin por módulo original. Se você precisa de três mudanças no componente de shipping, coloque todas em um mixin. Três arquivos separados dificultam acompanhar a cadeia de chamadas quando algo quebra.
- Dê ao arquivo o nome do original. Um
shipping-mixin.jsnum caminho que espelha o original deixa óbvio o que ele estende sem abrir a config. - Mantenha a regra de negócio fora do mixin. Coloque a lógica num módulo próprio e chame do mixin. O mixin fica como um adaptador fino, fácil de ajustar quando o original mudar.
- Revise os mixins a cada upgrade. Compare os métodos que você envolve com a nova versão do original. Quando o original renomeia ou remove um método, o código do core deixa de chamar o seu override, e nada avisa.
Conferindo se um mixin está carregado
Quando um mixin parece não fazer nada, confira nesta ordem:
- Abra o
requirejs-config.jsdo seu tema e locale empub/static/frontend/Vendor/theme/en_US/e procure o caminho do seu mixin. Se não estiver lá, o arquivo de config não está sendo pego ou o static content está desatualizado. - No console do navegador, rode
require.s.contexts._.config.config.mixinse procure a chave do módulo original. - Veja na aba Network se o arquivo do seu mixin chega a carregar.
- Confirme que a chave bate com o caminho que a página realmente requisita. Um módulo requisitado como
Magento_Checkout/js/view/shippingnão pega um mixin declarado para outro alias do mesmo arquivo. - Para widgets, confirme que o nome do widget bate com o do original.
Não existe ordem de execução configurável para mixins. Quando vários mixins miram o mesmo módulo, a ordem segue a ordem em que os arquivos de config são combinados, que segue o sequence dos módulos. Se o seu mixin precisa rodar depois de outro, adicione aquele módulo ao sequence do seu module.xml.
FAQ
Onde coloco o requirejs-config.js de um mixin?
Num módulo, em view/frontend/requirejs-config.js. Num tema, em web/requirejs-config.js ou na pasta do módulo dentro do tema. Depois faça o deploy de static content no modo production.
Posso usar um mixin no admin?
Sim. Declare em view/adminhtml/requirejs-config.js. Os mesmos três padrões valem para os módulos JavaScript do admin.
Como desativo o mixin de outro módulo?
Declare o mesmo módulo original e o mesmo caminho de mixin no seu próprio requirejs-config.js com o valor false, e garanta que o seu módulo carregue depois do outro por meio do sequence no module.xml.
Mixins funcionam no Hyvä?
Não. O Hyvä não usa RequireJS no storefront, então mixins do Luma não rodam lá. Os módulos de compatibilidade do Hyvä substituem esse JavaScript por código em Alpine.js.
Atendo como freelancer em Magento e Adobe Commerce, incluindo JavaScript de checkout e customizações no Luma. Se precisa de ajuda com isso, fale comigo.
Artigos relacionados
Escrevendo componentes Alpine.js no Hyvä: padrões que escalam
Como escrever componentes Alpine.js no Hyvä: passar dados do PHP, view models, eventos de customer data, form keys, eventos entre componentes, CSP e templates limpos.
Bundling de JavaScript no Magento 2: o que realmente acelera
Bundling nativo, merge, minificação e Magepack no Magento 2 comparados, com as configurações a usar, como medir o resultado e quando o bundling atrapalha.
UI components e Knockout.js no Magento 2: o básico
Introdução prática a UI components e Knockout.js no Magento 2: uiComponent, templates, observables, imports e exports, jsLayout e o uiRegistry.
Core Web Vitals no Magento 2: como corrigir LCP, INP e CLS
Onde lojas Magento 2 com Luma perdem nos Core Web Vitals e como corrigir LCP, INP e CLS: resposta do servidor, CSS, fontes, imagens, galeria e scripts de terceiros.