Magento 2: Como / Onde está limitada a função de bloqueio `getTemplate`?

19

Muitas páginas de back-end do Magento contêm o seguinte em seu código-fonte

<!-- ko template: getTemplate() --><!-- /ko -->

Entendo (ou acho que sim) que <!-- ko templateé uma ligação de modelo sem contêiner KnockoutJS .

O que não está claro para mim é: em que contexto a getTemplate()função é chamada? Nos exemplos que vejo online, geralmente há um objeto javascript após o template:. Estou assumindo que getTemplateé uma função javascript que retorna um objeto - mas não há nenhuma função javascript global nomeada getTemplate.

Onde está getTemplateligado? Ou, possivelmente, uma pergunta melhor: onde a ligação do aplicativo KnockoutJS acontece em uma página de back-end do Magento?

Estou interessado nisso do ponto de vista puro de HTML / CSS / Javascript. Eu sei que o Magento 2 tem muitas abstrações de configuração, portanto (em teoria) os desenvolvedores não precisam se preocupar com os detalhes da implementação. Estou interessado nos detalhes da implementação.

Alan Storm
fonte

Respostas:

38

O código PHP para um componente de interface do usuário renderiza uma inicialização em javascript parecida com esta

<script type="text/x-magento-init">
    {
        "*": {
            "Magento_Ui/js/core/app":{
                "types":{...},
                "components":{...},
            }
        }
    }
</script>       

Esse bit de código na página significa que o Magento chamará o Magento_Ui/js/core/appmódulo RequireJS para buscar um retorno de chamada e, em seguida, chamará esse retorno de chamada que passa no {types:..., components:...}objeto JSON como argumento ( dataabaixo)

#File: vendor/magento/module-ui/view/base/web/js/core/app.js
define([
    './renderer/types',
    './renderer/layout',
    'Magento_Ui/js/lib/ko/initialize'
], function (types, layout) {
    'use strict';

    return function (data) {
        types.set(data.types);
        layout(data.components);
    };
});

O objeto de dados contém todos os dados necessários para renderizar o componente da interface do usuário, bem como uma configuração que vincula determinadas seqüências de caracteres com determinados módulos do Magento RequireJS. Esse mapeamento acontece nos módulos RequireJS typese layout. O aplicativo também carrega a Magento_Ui/js/lib/ko/initializebiblioteca RequireJS. O initializemódulo inicia a integração KnockoutJS do Magento.

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
/** Loads all available knockout bindings, sets custom template engine, initializes knockout on page */

#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js
define([
    'ko',
    './template/engine',
    'knockoutjs/knockout-repeat',
    'knockoutjs/knockout-fast-foreach',
    'knockoutjs/knockout-es5',
    './bind/scope',
    './bind/staticChecked',
    './bind/datepicker',
    './bind/outer_click',
    './bind/keyboard',
    './bind/optgroup',
    './bind/fadeVisible',
    './bind/mage-init',
    './bind/after-render',
    './bind/i18n',
    './bind/collapsible',
    './bind/autoselect',
    './extender/observable_array',
    './extender/bound-nodes'
], function (ko, templateEngine) {
    'use strict';

    ko.setTemplateEngine(templateEngine);
    ko.applyBindings();
});

Cada indivíduo bind/... módulo RequireJS configura uma única ligação personalizada para Knockout.

Os extender/...módulos RequireJS incluem alguns métodos auxiliares nos objetos KnockoutJS nativos.

O Magento também estende a funcionalidade do mecanismo de modelo javascript do Knockout no ./template/engine módulo RequireJS.

Finalmente, o Magento chama applyBindings()o objeto KnockoutJS. Normalmente é onde um programa Knockout vincula um modelo de exibição à página HTML - no entanto, o Magento chamaapplyBindings sem um modelo de visualização. Isso significa que o Knockout começará a processar a página como uma exibição, mas sem dados vinculados.

Em uma configuração de Knockout de estoque, isso seria um pouco bobo. No entanto, devido às ligações personalizadas do Knockout mencionadas anteriormente, há muitas oportunidades para o Knockout fazer as coisas.

Estamos interessados ​​na ligação do escopo . Você pode ver isso neste HTML, também renderizado pelo sistema PHP UI Component.

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
    <div data-role="spinner" data-component="customer_listing.customer_listing.customer_columns" class="admin__data-grid-loading-mask">
        <div class="spinner">
            <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
        </div>
    </div>
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
    </script>
</div>

Especificamente, o data-bind="scope: 'customer_listing.customer_listing'">atributo Quando o Magento começar applyBindings, o Knockout verá essa scopeligação personalizada e invocará o ./bind/scopemódulo RequireJS. A capacidade de aplicar uma ligação personalizada é pura KnockoutJS. A implementação da ligação do escopo é algo que a Magento Inc. fez.

A implementação da ligação do escopo está em

#File: vendor/magento/module-ui/view/base/web/js/lib/ko/bind/scope.js

A parte importante deste arquivo está aqui

var component = valueAccessor(),
    apply = applyComponents.bind(this, el, bindingContext);

if (typeof component === 'string') {
    registry.get(component, apply);
} else if (typeof component === 'function') {
    component(apply);
}

Sem entrar em detalhes demais, o registry.getmétodo extrairá um objeto já gerado usando a string na componentvariável como um identificador e passará para o applyComponentsmétodo como o terceiro parâmetro. O identificador de sequência é o valor de scope:(customer_listing.customer_listing acima)

Dentro applyComponents

function applyComponents(el, bindingContext, component) {
    component = bindingContext.createChildContext(component);

    ko.utils.extend(component, {
        $t: i18n
    });

    ko.utils.arrayForEach(el.childNodes, ko.cleanNode);

    ko.applyBindingsToDescendants(component, el);
}

a chamada para createChildContextcriará o que é, essencialmente, um novo objeto viewModel com base no objeto componente já instanciado e o aplicará a todo o elemento descendente do original divusado data-bind=scope:.

Então, qual é o objeto componente já instanciado ? Lembra da ligação para layoutvoltar app.js?

#File: vendor/magento/module-ui/view/base/web/js/core/app.js

layout(data.components);

A layoutfunção / módulo descerá para o passado data.components(novamente, esses dados provêm do objeto passado via text/x-magento-init). Para cada objeto que encontrar, ele procurará um configobjeto e, nesse objeto de configuração, procurará uma componentchave. Se encontrar uma chave de componente, será

  1. Use RequireJSpara retornar uma instância do módulo - como se o módulo fosse chamado em requirejs/ definedependency.

  2. Chame essa instância de módulo como um construtor javascript

  3. Armazene o objeto resultante no registryobjeto / módulo

Então, isso é muito necessário. Aqui está uma revisão rápida, usando

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
    <div data-role="spinner" data-component="customer_listing.customer_listing.customer_columns" class="admin__data-grid-loading-mask">
        <div class="spinner">
            <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
        </div>
    </div>
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
    </script>
</div>

como ponto de partida. O scopevalor é customer_listing.customer_listing.

Se olharmos para o objeto JSON a partir da text/x-magento-initinicialização

{
    "*": {
        "Magento_Ui/js/core/app": {
            /* snip */
            "components": {
                "customer_listing": {
                    "children": {
                        "customer_listing": {
                            "type": "customer_listing",
                            "name": "customer_listing",
                            "children": /* snip */
                            "config": {
                                "component": "uiComponent"
                            }
                        },
                        /* snip */
                    }
                }
            }
        }
    }
}

Vemos que o components.customer_listing.customer_listingobjeto tem um configobjeto e esse objeto de configuração tem um componentobjeto definido uiComponent. A uiComponentsequência é um módulo RequireJS. De fato, é um alias RequireJS que corresponde ao Magento_Ui/js/lib/core/collectionmódulo.

vendor/magento/module-ui/view/base/requirejs-config.js
14:            uiComponent:    'Magento_Ui/js/lib/core/collection',

No layout.jsMagento, o código de execução é equivalente ao seguinte.

//The actual code is a bit more complicated because it
//involves jQuery's promises. This is already a complicated 
//enough explanation without heading down that path

require(['Magento_Ui/js/lib/core/collection'], function (collection) {    
    object = new collection({/*data from x-magento-init*/})
}

Para os realmente curiosos, se você der uma olhada no modelo de coleção e seguir seu caminho de execução, descobrirá que esse collectioné um objeto javascript aprimorado pelo lib/core/element/elementmódulo e pelo lib/core/classmódulo. A pesquisa dessas personalizações está além do escopo desta resposta.

Uma vez instanciado, layout.jsarmazena isso objectno registro. Isso significa que quando o Knockout começa a processar as ligações e encontra a scopeligação personalizada

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
    <!-- snip -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!-- snip -->
</div>

O Magento buscará esse objeto de volta no registro e o vinculará como o modelo de exibição das coisas dentro do div. Em outras palavras, o getTemplatemétodo chamado quando Knockout chama a ligação sem marca ( <!-- ko template: getTemplate() --><!-- /ko -->) é o getTemplatemétodo no new collectionobjeto.

Alan Storm
fonte
1
Eu odeio apenas fazer a pergunta 'por que' à sua resposta, para uma pergunta mais focada, o que o M2 ganha ao usar esse sistema (aparentemente complicado) para chamar modelos de KO?
circlesix
1
@circlesix Faz parte de um sistema maior para renderização a <uiComponents/>partir do sistema XML de layout. Os benefícios que obtêm é a capacidade de trocar os modelos de exibição na mesma página por um conjunto diferente de tags.
Alan Storm
16
Não sei se ri ou choro! Que bagunça.
Koosa
8
Eu acho que eles estão cavando sua própria cova. Se eles continuam complicando as coisas como esta, as empresas vão parar de usá-lo por causa dos custos de desenvolvimento
Marián Zeke Šedaj
2
Eu gasto apenas cerca de 5 horas tentando descobrir como vincular um comportamento personalizado a um formulário que é processado por toda essa "mágica". Uma parte do problema é que essa estrutura altamente genérica precisa que você passe por várias camadas até ter a chance de entender como fazer as coisas. Também rastrear de onde vem uma determinada configuração se torna incrivelmente entediante.
greenone83
12

A ligação para qualquer um dos modelos JS knockout ocorre nos arquivos .xml do módulo. Usando o módulo Checkout como exemplo, você pode encontrar a configuração do contentmodelo emvendor/magento/module-checkout/view/frontend/layout/default.xml

<block class="Magento\Checkout\Block\Cart\Sidebar" name="minicart" as="minicart" after="logo" template="cart/minicart.phtml">
    <arguments>
        <argument name="jsLayout" xsi:type="array">
            <item name="types" xsi:type="array"/>
                <item name="components" xsi:type="array">
                    <item name="minicart_content" xsi:type="array">
                        <item name="component" xsi:type="string">Magento_Checkout/js/view/minicart</item>
                            <item name="config" xsi:type="array">
                                <item name="template" xsi:type="string">Magento_Checkout/minicart/content</item>
                            </item>

Nesse arquivo, você pode ver que a classe de bloco possui nós que definem o "jsLayout" e chamam o <item name="minicart_content" xsi:type="array">. É uma espécie de rodízio de lógica, mas se você estiver dentro vendor/magento/module-checkout/view/frontend/templates/cart/minicart.phtml, verá esta linha:

<div id="minicart-content-wrapper" data-bind="scope: 'minicart_content'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>

Portanto, a ligação de dados direciona onde procurar qualquer modelo aninhado; nesse caso, é o Magento_Checkout/js/view/minicartda vendor/magento/module-checkout/view/frontend/web/js/view/minicart.jslógica (ou MV no sistema de modelo de visualização e visualização de nocautes) e você tem Magento_Checkout/minicart/content(ou V no modelo de visualização de visualização de modelos e nocautes) sistema) para a chamada de modelo. Portanto, o modelo que está sendo puxado neste local é vendor/magento/module-checkout/view/frontend/web/template/minicart/content.html.

Realmente, não é difícil descobrir uma vez que você se acostuma a procurar nos .xml. Aprendi a maior parte disso aqui, se você conseguir passar do inglês quebrado. Mas até agora sinto que a integração do Knockout é a parte menos documentada do M2.

círculos
fonte
2
Informações úteis, então +1, mas de acordo com a pergunta, eu sei que o Magento tem abstrações para lidar com isso - mas estou curioso sobre os detalhes da implementação. ie - quando você configura algo nesse arquivo XML, o magento faz outra coisa para garantir que seus valores configurados façam uma terceira coisa . Estou interessado na outra coisa e na terceira coisa.
Alan Storm