How Are the Things Magento 2 calls "mixins" Implemented?
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
tomixins!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.
To round out Denis Rul's answer.
So, if you look at a Magento page, here are the three <script/>
tags that load Magento.
<script type="text/javascript" src="http://magento.example.com/pub/static/frontend/Magento/luma/en_US/requirejs/require.js"></script>
<script type="text/javascript" src="http://magento.example.com/pub/static/frontend/Magento/luma/en_US/mage/requirejs/mixins.js"></script>
<script type="text/javascript" src="http://magento.example.com/pub/static/_requirejs/frontend/Magento/luma/en_US/requirejs-config.js"></script>
This is RequireJS itself (require.js
), the mixins.js
plugin, and the merged RequireJS configuration (requirejs-config.js
).
The mixins.js
file defines a RequireJS plugin. This plugin is responsible for loading and calling the RequireJS modules that listen for other RequireJS module's instantiation.
This plugin also contains a requirejs program after it defines the mixin plugin.
require([
'mixins'
], function (mixins) {
'use strict';
//...
/**
* Overrides global 'require' method adding to it dependencies modfication.
*/
window.require = function (deps, callback, errback, optional) {
//...
};
//...
window.define = function (name, deps, callback) {
//...
};
window.requirejs = window.require;
});
This second program loads the just defined mixins
plugin as a dependency, and then redefines the global require
, define
and requirejs
functions. This redefinition is what allows the "not really a mixin" system to hook into RequireJS module's initial instantiation before passing things back to the regular functions.