How Magento 2 Mixins Are Implemented

magento2mixinsrequirejs

Magento 2's RequireJS based object systems contains a feature called "mixins". A Magento 2 mixin isn't what a software engineer would normally think of as a mixin/trait. Instead, a Magento 2 mixin allows you to modify the object/value returned by a RequireJS module before that object/value is used by the main program. You configure a Magento 2 mixin like this (via a requirejs-config.js file)

var config = {
    'config':{
        'mixins': {
            //the module to modify
            'Magento_Checkout/js/view/form/element/email': {
                //your module that will do the modification
                'Pulsestorm_RequireJsRewrite/hook':true
            }
        }
    }
};

Then, you need to have hook.js (or whatever RequireJS module you've configured),

define([], function(){
    console.log("Hello");
    return function(theObjectReturnedByTheModuleWeAreHookingInto){
        console.log(theObjectReturnedByTheModuleWeAreHookingInto);
        console.log("Called");
        return theObjectReturnedByTheModuleWeAreHookingInto;
    };
});

return a function. Magento will call this function, passing in a reference to the "module" you want to modify. In our example this will be the object returned by the RequireJS module Magento_Checkout/js/view/form/element/email. This may also be a function, or even a scaler value (depending on what the RequireJS module returns).

This system appears to be called mixins because it allows you to create mixin like behavior if the object returned by the original RequireJS module supports the extend method.

define([], function(){
    'use strict';
    console.log("Hello");

    var mixin = {
        ourExtraMethod = function(){
            //...
        }
    };

    return function(theObjectReturnedByTheModuleWeAreHookingInto){
        console.log(theObjectReturnedByTheModuleWeAreHookingInto);
        console.log("Called");


        return theObjectReturnedByTheModuleWeAreHookingInto.extend(mixin);
    };
});

However, the system itself is just a way to hook into module object creation.

Preamble finished — does anyone know how Magento has implemented this functionality? The RequireJS website doesn't seem to mention mixins (although Google thinks you might want RequireJS's plugin page).

Outside of requirejs-config.js files, Magento 2's core javascript only mentions mixins in three files

$ find vendor/magento/ -name '*.js' | xargs ack mixins
vendor/magento/magento2-base/lib/web/mage/apply/main.js
73:                            if (obj.mixins) {
74:                                require(obj.mixins, function () {
79:                                    delete obj.mixins;

vendor/magento/magento2-base/lib/web/mage/apply/scripts.js
39:            if (_.has(obj, 'mixins')) {
41:                data[key].mixins = data[key].mixins || [];
42:                data[key].mixins = data[key].mixins.concat(obj.mixins);
43:                delete obj.mixins;

vendor/magento/magento2-base/lib/web/mage/requirejs/mixins.js
5:define('mixins', [
24:     * Adds 'mixins!' prefix to the specified string.
30:        return 'mixins!' + name;
76:     * Iterativly calls mixins passing to them
80:     * @param {...Function} mixins
84:        var mixins = Array.prototype.slice.call(arguments, 1);
86:        mixins.forEach(function (mixin) {
96:         * Loads specified module along with its' mixins.
102:                mixins   = this.getMixins(path),
103:                deps     = [name].concat(mixins);
111:         * Retrieves list of mixins associated with a specified module.
114:         * @returns {Array} An array of paths to mixins.
118:                mixins = config[path] || {};
120:            return Object.keys(mixins).filter(function (mixin) {
121:                return mixins[mixin] !== false;
126:         * Checks if specified module has associated with it mixins.
137:         * the 'mixins!' plugin prefix if it's necessary.
172:    'mixins'
173:], function (mixins) {
237:        deps = mixins.processNames(deps, context);
252:            queueItem[1] = mixins.processNames(lastDeps, context);

The mixins.js file appears to be a RequireJS plugin (based on the !... mentions in the comments — is this right?) but it's not 100% clear when main.js or scripts.js are invoked by Magento, or how the custom mixins configuration makes it from requirejs-config.js into the listener/hook system described above.

Does anyone have an explanation for how this system was/is implemented/architected, with an eye towards being able to debug why a "mixin" may or may not be applied?

Best Answer

I'd like to go straight to your questions and then I'll try to make it clear on what you can actually do with the mixins plugin. So, first things first.

Implementation

The main thing here is the ability of any RequireJS plugin to completely take over the loading process of certain files. This allows to modify the export value of a module before it will be passed as a resolved dependency.

Take a look at this sketchy implementation of what Magento custom mixins plugin is actually is:

// RequireJS config object.
// Like this one: app/code/Magento/Theme/view/base/requirejs-config.js
{
    //...

    // Every RequireJS plugin is a module and every module can
    // have it's configuration.
    config: {
        sampleMixinPlugin: {
            'path/to/the/sampleModule': ['path/to/extension']
        }
    }
}

define('sampleMixinPlugin', [
    'module'
] function (module) {
    'use strict';

    // Data that was defined in the previous step.
    var mixinsMap = module.config();

    return {
        /**
         * This method will be invoked to load a module in case it was requested
         * with a 'sampleMixinPlugin!' substring in it's path,
         * e.g 'sampleMixinPlugin!path/to/the/module'.
         */
        load: function (name, req, onLoad) {
            var mixinsForModule = [],
                moduleUrl = req.toUrl(name),
                toLoad;

            // Get a list of mixins that need to be applied to the module.
            if (name in mixinsMap) {
                mixinsForModule = mixinsMap[name];
            }

            toLoad = [moduleUrl].concat(mixinsForModule);

            // Load the original module along with mixins for it.
            req(toLoad, function (moduleExport, ...mixinFunctions) {
                // Apply mixins to the original value exported by the sampleModule.
                var modifiedExport = mixinFunctions.reduce(function (result, mixinFn) {
                        return mixinFn(result);
                }, moduleExport);

                // Tell RequireJS that this is what was actually loaded.
                onLoad(modifiedExport);
            });
        }
    }
});

The last and the most challenging part is to dynamically prepend the 'sampleMixinPlugin!' substring to the requested modules. To do this we intercept define and require invocations and modify the list of dependencies before they will be processed by the original RequireJS load method. It's a little bit tricky and I'd recommend to look at the implementation lib/web/mage/requirejs/mixins.js if you wanna how it works.

Debugging

I'd recommend this steps:

  • Make sure that the configuration for 'mixins!' plugin is actually there.
  • Check that the path to a module is being modified. I.e. it turns from path/to/module to mixins!path/to/module.

And the last but not least, requiresjs/mixins.js has nothing to do with the main.js or script.js modules as they can only extend the configuration being passed from the data-mage-init attribute:

<div data-mage-init='{
    "path/to/module": {
        "foo": "bar",
        "mixins": ["path/to/configuration-modifier"]
    }
}'></div>

I mean that the former two files don't mess with the value returned by a module, instead they pre-process configuration of an instance.

Usage Examples

To begin with I'd like to set the record straight that so called "mixins" (you're right about the misnaming) actually allow to modify the exported value of a module in any way you want. I'd say that this is a way more generic mechanism.

Here is a quick sample of adding extra functionality to the function being exported by a module:

// multiply.js
define(function () {
    'use strict';

    /**
     * Multiplies two numeric values.
     */
    function multiply(a, b) {
        return a * b;
    }

    return multiply;
});

// extension.js
define(function () {
    'use strict';

    return function (multiply) {
        // Function that allows to multiply an arbitrary number of values.
        return function () {
            var args = Array.from(arguments);

            return args.reduce(function (result, value) {
                return multiply(result, value);
            }, 1);
        };
    };
});

// dependant.js
define(['multiply'], function (multiply) {
    'use strict';

    console.log(multiply(2, 3, 4)); // 24
});

You can implement an actual mixin for any object/function returned by a module and you don't need to depend on the extend method at all.

Extending a constructor function:

// construnctor.js
define(function () {
    'use strict';

    function ClassA() {
        this.property = 'foo';
    }

    ClassA.prototype.method = function () {
        return this.property + 'bar';
    }

    return ClassA;
});

// mixin.js
define(function () {
    'use strict';

    return function (ClassA) {
        var originalMethod = ClassA.prototype.method;

        ClassA.prototype.method = function () {
            return originalMethod.apply(this, arguments) + 'baz';
        };

        return ClassA;
    }
});

I hope that this answers your questions.

Regards.

Related Topic