Magento 2: Comment / Où est liée la fonction de désactivation `getTemplate`?

19

De nombreuses pages backend Magento contiennent les éléments suivants dans leur code source

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

Je comprends (ou je pense que je le fais?) Qu'il <!-- ko templates'agit d'une liaison de modèle sans conteneur KnockoutJS .

Ce qui n'est pas clair pour moi, c'est - dans quel contexte la getTemplate()fonction est-elle appelée? Dans les exemples que j'ai vus en ligne, il y a généralement un objet javascript après le template:. Je suppose quegetTemplate c'est une fonction javascript qui renvoie un objet - mais il n'y a pas de fonction javascript globale nommée getTemplate.

Où est getTemplatelié? Ou, peut-être une meilleure question, où la liaison d'application KnockoutJS se produit-elle sur une page principale de Magento?

Cela m'intéresse d'un point de vue pur HTML / CSS / Javascript. Je sais que Magento 2 a de nombreuses abstractions de configuration, donc (en théorie) les développeurs n'ont pas à se soucier des détails de l'implémentation. Je suis intéressé par les détails d'implémentation.

Alan Storm
la source

Réponses:

38

Le code PHP pour un composant d'interface utilisateur rend une initialisation javascript qui ressemble à ceci

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

Ce bit de code dans la page signifie que Magento invoquera le Magento_Ui/js/core/appmodule RequireJS pour récupérer un rappel, puis appellera ce rappel en passant dans l' {types:..., components:...}objet JSON comme argument ( dataci-dessous)

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

L'objet de données contient toutes les données nécessaires au rendu du composant d'interface utilisateur, ainsi qu'une configuration qui relie certaines chaînes à certains modules Magento RequireJS. Ce mappage se produit dans les modules typeset layoutRequireJS. L'application charge également la Magento_Ui/js/lib/ko/initializebibliothèque RequireJS. Le initializemodule lance l'intégration 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();
});

Chaque bind/...module RequireJS individuel configure une liaison personnalisée unique pour Knockout.

le extender/... modules RequireJS ajoutent des méthodes d'assistance aux objets KnockoutJS natifs.

Magento étend également les fonctionnalités du moteur de modèle javascript de Knockout dans le ./template/enginemodule RequireJS.

Enfin, Magento fait appel applyBindings()à l'objet KnockoutJS. C'est normalement là qu'un programme Knockout lierait un modèle de vue à la page HTML - cependant, Magento appelle applyBindings sans modèle de vue. Cela signifie que Knockout commencera à traiter la page comme une vue, mais sans données liées.

Dans une configuration stock Knockout, ce serait un peu idiot. Cependant, en raison des liaisons Knockout personnalisées mentionnées précédemment, Knockout a de nombreuses possibilités de faire des choses.

Nous sommes intéressés par la liaison de portée . Vous pouvez le voir dans ce HTML, également rendu par le système de composants 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>

Plus précisément, l' data-bind="scope: 'customer_listing.customer_listing'">attribut. Lorsque Magento démarre applyBindings, Knockout verra cette scopeliaison personnalisée et invoquera le ./bind/scopemodule RequireJS. La possibilité d'appliquer une liaison personnalisée est pure KnockoutJS. L' implémentation de la liaison de portée est quelque chose que Magento Inc. a fait.

La mise en œuvre de la liaison de portée est à

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

La partie importante de ce fichier est ici

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

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

Sans entrer trop dans les détails, la registry.getméthode extraira un objet déjà généré en utilisant la chaîne dans la componentvariable comme identifiant et la passera à la applyComponentsméthode comme troisième paramètre. L'identifiant de chaîne est la valeur de scope:(customer_listing.customer_listing ci-dessus)

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

l'appel à createChildContextcréera ce qui est, essentiellement, un nouvel objet viewModel basé sur l'objet composant déjà instancié, puis l'appliquera à tous les éléments descendants de l'original divutilisé data-bind=scope:.

Alors, quel est l' objet composant déjà instancié ? Rappelez-vous l'appel à layoutrevenir app.js?

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

layout(data.components);

La layoutfonction / module descendra dans le passé data.components(encore une fois, ces données proviennent de l'objet passé via text/x-magento-init). Pour chaque objet qu'il trouve, il cherchera un configobjet, et dans cet objet de configuration, il cherchera une componentclé. S'il trouve une clé de composant, il

  1. Permet RequireJSde renvoyer une instance de module - comme si le module était appelé dans une dépendance requirejs/ define.

  2. Appelez cette instance de module en tant que constructeur javascript

  3. Stocker l'objet résultant dans l' registryobjet / module

Donc, c'est beaucoup à retenir. Voici un bref examen, en utilisant

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

comme point de départ. La scopevaleur est customer_listing.customer_listing.

Si nous regardons l'objet JSON depuis l' text/x-magento-initinitialisation

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

Nous voyons que l' components.customer_listing.customer_listingobjet a un configobjet, et cet objet de configuration a un componentobjet qui est défini sur uiComponent. La uiComponentchaîne est un module RequireJS. En fait, c'est un alias RequireJS qui correspond au Magento_Ui/js/lib/core/collectionmodule.

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

Dans layout.js, Magento a exécuté un code équivalent à ce qui suit.

//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*/})
}

Pour les plus curieux, si vous jetez un œil au modèle de collection et suivez son chemin d'exécution, vous découvrirez qu'il collections'agit d'un objet javascript qui a été amélioré à la fois par le lib/core/element/elementmodule et le lib/core/classmodule. La recherche de ces personnalisations dépasse le cadre de cette réponse.

Une fois instancié, layout.jsstocke cela objectdans le registre. Cela signifie que Knockout commence à traiter les liaisons et rencontre la scopeliaison personnalisée

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

Magento récupérera cet objet du registre et le liera comme modèle de vue pour les choses à l'intérieur de div. En d'autres termes, la getTemplateméthode qui est appelée lorsque Knockout appelle la liaison sans étiquette ( <!-- ko template: getTemplate() --><!-- /ko -->) est la getTemplateméthode sur l' new collectionobjet.

Alan Storm
la source
1
Je déteste simplement poser la question «pourquoi» à votre réponse, donc une question plus ciblée serait, qu'est-ce que M2 gagne en utilisant ce système (apparemment compliqué) pour appeler des modèles KO?
circlesix
1
@circlesix Il fait partie d'un plus grand système de rendu à <uiComponents/>partir du système XML de mise en page. Les avantages qu'ils obtiennent sont la possibilité d'échanger des modèles de vue sur la même page pour un ensemble différent de balises.
Alan Storm
16
Je ne sais pas si je dois rire ou pleurer! Quel bordel.
koosa
8
Je pense qu'ils creusent leur propre tombe. S'ils continuent de compliquer des choses comme ça, les entreprises cesseront de l'utiliser en raison des coûts de développement
Marián Zeke Šedaj
2
Je viens de passer environ 5 heures à essayer de comprendre comment lier un comportement personnalisé à une forme rendue par toute cette "magie". Une partie du problème est que ce cadre hautement générique nécessite que vous passiez par une tonne de couches jusqu'à ce que vous ayez une chance de comprendre comment faire les choses. Le suivi de l'origine d'une certaine configuration devient également extrêmement fastidieux.
greenone83
12

La liaison pour l'un des modèles JS knockout se produit dans les fichiers .xml du module. En utilisant le module Checkout comme exemple, vous pouvez trouver la configuration du contentmodèle dansvendor/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>

Dans ce fichier, vous pouvez voir que la classe de bloc a des nœuds qui définissent le "jsLayout" et appellent le <item name="minicart_content" xsi:type="array">. C'est un peu un round robin de logique, mais si vous êtesvendor/magento/module-checkout/view/frontend/templates/cart/minicart.phtml vous verrez cette ligne:

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

Ainsi, la liaison de données indique où chercher tout modèle imbriqué, dans ce cas, c'est le Magento_Checkout/js/view/minicartdevendor/magento/module-checkout/view/frontend/web/js/view/minicart.js la logique (ou MV dans KOs Model-View-Vue système modèle) et vous avez Magento_Checkout/minicart/content(ou V KOs Model-View-View modèle système) pour l'appel de modèle. Donc, le modèle qui est tiré à cet endroit est vendor/magento/module-checkout/view/frontend/web/template/minicart/content.html.

Vraiment, il n'est pas trop difficile de comprendre une fois que vous vous êtes habitué à regarder dans les fichiers .xml. La plupart de cela, j'ai appris ici si vous pouvez vous frayer un chemin à travers l'anglais brisé. Mais jusqu'à présent, j'ai l'impression que l'intégration Knockout est la partie la moins documentée de M2.

cerclesix
la source
2
Informations utiles, donc +1, mais à la question, je sais que Magento a des abstractions pour gérer cela - mais je suis curieux de connaître les détails de l'implémentation eux-mêmes. c'est-à-dire - lorsque vous configurez quelque chose dans ce fichier XML, magento fait autre chose pour vous assurer que vos valeurs configurées font une troisième chose . Je m'intéresse à autre chose et à la troisième chose.
Alan Storm