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:
The last and the most challenging part is to dynamically prepend the 'sampleMixinPlugin!' substring to the requested modules. To do this we intercept
define
andrequire
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 implementationlib/web/mage/requirejs/mixins.js
if you wanna how it works.Debugging
I'd recommend this steps:
path/to/module
tomixins!path/to/module
.And the last but not least,
requiresjs/mixins.js
has nothing to do with themain.js
orscript.js
modules as they can only extend the configuration being passed from thedata-mage-init
attribute: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:
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:
I hope that this answers your questions.
Regards.