Magento 2: ¿Cómo / dónde está vinculada la función knockout `getTemplate`?

19

Muchas páginas de backend de Magento contienen lo siguiente en su código fuente

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

Entiendo (¿o creo que lo hago?) Que <!-- ko templatees un enlace de plantilla sin contenedor KnockoutJS .

Lo que no está claro para mí es: ¿en qué contexto se getTemplate()llama la función? En los ejemplos que veo en línea, generalmente hay un objeto javascript después de template:. Supongo que getTemplatees una función de JavaScript que devuelve un objeto, pero no hay una función global de JavaScript nombrada getTemplate.

¿Dónde está getTemplateobligado? O, posiblemente, una mejor pregunta, ¿dónde ocurre el enlace de la aplicación KnockoutJS en una página de backend de Magento?

Estoy interesado en esto desde un punto de vista HTML / CSS / Javascript puro. Sé que Magento 2 tiene muchas abstracciones de configuración, por lo que (en teoría) los desarrolladores no necesitan preocuparse por los detalles de implementación. Estoy interesado en los detalles de implementación.

Alan Storm
fuente

Respuestas:

38

El código PHP para un componente de UI representa una inicialización de JavaScript que se ve así

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

Este bit de código en la página significa que Magento invocará el Magento_Ui/js/core/appmódulo RequireJS para recuperar una devolución de llamada, y luego llamará a esa devolución de llamada que pasa en el {types:..., components:...}objeto JSON como argumento (a datacontinuación)

#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);
    };
});

El objeto de datos contiene todos los datos necesarios para representar el componente de la interfaz de usuario, así como una configuración que vincula ciertas cadenas con ciertos módulos Magento RequireJS. Esa asignación ocurre en los módulos typesy layoutRequireJS. La aplicación también carga la Magento_Ui/js/lib/ko/initializebiblioteca RequireJS. El initializemódulo inicia la integración KnockoutJS de 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 bind/...módulo RequireJS individual configura un enlace personalizado único para Knockout.

Los extender/...módulos RequireJS agregan algunos métodos auxiliares a los objetos nativos de KnockoutJS.

Magento también extiende la funcionalidad del motor de plantillas javascript de Knockout en el ./template/enginemódulo RequireJS.

Finalmente, Magento llama applyBindings()al objeto KnockoutJS. Esto es normalmente donde un programa Knockout vincularía un modelo de vista a la página HTML; sin embargo, Magento llama applyBindings sin un modelo de vista. Esto significa que Knockout comenzará a procesar la página como una vista, pero sin datos vinculados.

En una configuración original de Knockout, esto sería un poco tonto. Sin embargo, debido a los enlaces de Knockout personalizados mencionados anteriormente, hay muchas oportunidades para que Knockout haga cosas.

Estamos interesados ​​en el alcance vinculante. Puede ver eso en este HTML, también representado por el sistema de componentes PHP UI.

<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>

Específicamente, el data-bind="scope: 'customer_listing.customer_listing'">atributo. Cuando se inicie Magento applyBindings, Knockout verá este scopeenlace personalizado e invocará el ./bind/scopemódulo RequireJS. La capacidad de aplicar un enlace personalizado es KnockoutJS puro. La implementación del enlace de alcance es algo que Magento Inc. ha hecho.

La implementación del enlace de alcance está en

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

Lo importante en este archivo está aquí

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

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

Sin entrar demasiado en los detalles, el registry.getmétodo extraerá un objeto ya generado utilizando la cadena en la componentvariable como identificador, y lo pasará al applyComponentsmétodo como el tercer parámetro. El identificador de cadena es el valor de scope:( customer_listing.customer_listingarriba)

En 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);
}

la llamada a createChildContextcreará lo que es, esencialmente, un nuevo objeto viewModel basado en el objeto componente ya instanciado, y luego lo aplicará a todos los elementos descendientes del original divque se usó data-bind=scope:.

Entonces, ¿cuál es el objeto componente ya instanciado ? ¿Recuerdas la llamada para layoutvolver app.js?

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

layout(data.components);

La layoutfunción / módulo descenderá al pasado data.components(nuevamente, estos datos provienen del objeto pasado a través de text/x-magento-init). Para cada objeto que encuentre, buscará un configobjeto, y en ese objeto de configuración buscará una componentclave. Si encuentra una clave de componente, lo hará

  1. Utilícelo RequireJSpara devolver una instancia de módulo, como si se llamara al módulo en una dependencia requirejs/ define.

  2. Llame a esa instancia de módulo como un constructor de JavaScript

  3. Almacene el objeto resultante en el registryobjeto / módulo

Entonces, eso es mucho para asimilar. Aquí hay una revisión 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 punto de partida El scopevalor es customer_listing.customer_listing.

Si miramos el objeto JSON desde la text/x-magento-initinicialización

{
    "*": {
        "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 el components.customer_listing.customer_listingobjeto tiene un configobjeto, y ese objeto de configuración tiene un componentobjeto establecido en uiComponent. La uiComponentcadena es un módulo RequireJS. De hecho, es un alias RequireJS que corresponde al Magento_Ui/js/lib/core/collectionmódulo.

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

En layout.js, Magento ha ejecutado un código que es equivalente al siguiente.

//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 los realmente curiosos, si echas un vistazo al modelo de colección y sigues su ruta de ejecución, descubrirás que collectiones un objeto javascript que ha sido mejorado tanto por el lib/core/element/elementmódulo como por el lib/core/classmódulo. Investigar estas personalizaciones está más allá del alcance de esta respuesta.

Una vez instanciado, layout.jsalmacena esto objecten el registro. Esto significa que cuando Knockout comienza a procesar los enlaces y encuentra el scopeenlace personalizado

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

Magento recuperará este objeto del registro y lo vinculará como el modelo de vista para las cosas dentro del div. En otras palabras, el getTemplatemétodo que se llama cuando Knockout invoca el enlace sin etiquetas ( <!-- ko template: getTemplate() --><!-- /ko -->) es el getTemplatemétodo en el new collectionobjeto.

Alan Storm
fuente
1
Odio hacer la pregunta 'por qué' a su respuesta, por lo que una pregunta más centrada sería, ¿qué gana M2 al usar este sistema (aparentemente complicado) para llamar plantillas KO?
circlesix
1
@circlesix Es parte de un sistema más grande para renderizar <uiComponents/>desde el sistema XML de diseño. Los beneficios que obtienen es la capacidad de intercambiar modelos de vista en la misma página por un conjunto diferente de etiquetas.
Alan Storm
16
¡No sé si reír o llorar! Que desastre.
koosa
8
Creo que están cavando su propia tumba. Si siguen complicando cosas como esta, las compañías dejarán de usarlo debido a los costos de desarrollo
Marián Zeke Šedaj
2
Acabo de pasar alrededor de 5 horas tratando de descubrir cómo vincular un comportamiento personalizado a una forma representada por toda esta "magia". Una parte del problema es que este marco altamente genérico necesita que pases por un montón de capas hasta que tengas la oportunidad de entender cómo hacer las cosas. También el seguimiento de dónde proviene una determinada configuración se vuelve increíblemente tedioso.
greenone83
12

El enlace para cualquiera de las plantillas JS noqueadas se produce en los archivos .xml del módulo. Usando el módulo Checkout como ejemplo, puede encontrar la configuración de la contentplantilla envendor/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>

En este archivo puede ver que la clase de bloque tiene nodos que definen el "jsLayout" y llaman al <item name="minicart_content" xsi:type="array">. Es un poco como un round robin de lógica, pero si estás dentro vendor/magento/module-checkout/view/frontend/templates/cart/minicart.phtmlverás esta línea:

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

Por lo que los datos se unen dirige dónde buscar cualquier plantilla anidada, en este caso es el Magento_Checkout/js/view/minicartde vendor/magento/module-checkout/view/frontend/web/js/view/minicart.jsla lógica (o MV en knockouts Modelo-Vista-Vista sistema de modelo) y usted tiene Magento_Checkout/minicart/content(o V en knockouts Modelo-Vista-View Model sistema) para la plantilla de llamada. Entonces, la plantilla que se está extrayendo en este lugar es vendor/magento/module-checkout/view/frontend/web/template/minicart/content.html.

Realmente no es difícil de entender una vez que te acostumbras a buscar en los archivos .xml. La mayoría de esto lo aprendí aquí si puedes pasar el inglés roto. Pero hasta ahora siento que la integración de Knockout es la parte menos documentada de M2.

Círculosix
fuente
2
Información útil, entonces +1, pero según la pregunta, sé que Magento tiene abstracciones para lidiar con esto, pero tengo curiosidad sobre los detalles de implementación en sí. es decir, cuando configura algo en ese archivo XML, magento hace algo más para asegurarse de que sus valores configurados hagan una tercera cosa . Estoy interesado en algo más y lo tercero.
Alan Storm