这篇要说的是 Magento 2 的 Mixins 。说它奇怪是因为 Mixins 这个命名不正确,有歧义,他其实是 RequireJS monkey patching 。(Monkey Patching 指的是在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。猴子补丁在代码运行时发挥作用,不会修改源码。)
本文的目标是教会大家用 Magento 2 js 方法安全地 overwrite js method。这已经超过了平常 RequireJS 工具的范畴。
我们将快速过一下几个概念,然后说一说 Magento js 系统相关的一些特性,最后讨论这个名称奇怪的 mixins 系统。
什么是 Mixin ?
Mixin 从某种角度看,是传统的类集成的替代方法。
传统的面向对象编程中,你可能像下面这样定义三个类:
class A
{
public function foo()
{
}
}
class B extends A
{
public function bar()
{
}
}
class C extends B
{
public function baz()
{
}
}
$object = new C;
这样 $object
对象拥有 baz
bar
和 foo
方法。
Mixins 提供了另一种方式。通过 Mixins ,你可以组合类的方法到你的类中。伪代码看起来是这样的:
class A
{
public function foo()
{
}
}
class B
{
public function bar()
{
}
}
class C
{
mixin A;
mixin B;
public function baz()
{
}
}
$object = new C;
注意这里没有继承,class C 从 class A 和 class B 中获得方法。
PHP 的 Traits就是一种简单的 mixin 系统。在 PHP 中,你必须显示地声明 traits ,然后在你地类中组合这些 traits 。Traits 自身不能被实例化,php 类也不能被当成 traits 用。
Ruby通过 “include” 来组合别的模块的方法。
有些语言比如 Python 则通过多重继承来实现 Mixin
Javascript and Mixins, Sitting in a Tree
和 classes 不同,不同语言关于 mixin 的语法应该怎么写并没有形成共识。有些语言有明确的 mixins 而另一些语言只有事实上的 mixins
Javascript 就是后者。js 不是基于类的语言,他是一种基于原型的语言。(所以可以说 js 没有类)
通过 underscore.js 的 extend
方法,你可以获得 mixin-like 的行为。像下面这样:
var a = {
foo:function(){
//...
}
};
var b = {
bar:function(){
//...
}
}
c = _.extend(a, b);
这样 c 对象拥有 foo 和 bar 方法。
比较有迷惑性的是,underscore.js 也有一个方法叫 mixin 不过这个方法是用来往 underscore js 对象自身加方法的。
Magento uiClass Objects
如果你已经阅读了 Alan Storm 的 UI Component 系列,那么你已经熟悉 Magento’s uiClass objects 了。 这些对象也有一个 extend
方法。这个方法看起来和 underscore.js 的 extend
方法一样。
var b = {
bar:function(){
//...
}
}
UiClass = requirejs('uiClass');
// class NewClass extends uiClass
var NewClass = UiClass.extend(b);
// class AnotherNewClass extends NewClass
var AnotherNewClass = NewClass.extend({});
var object = new NewClass;
object.bar();
不过,uiClass extend
方法的用途有点不一样。uiClass extend
的目的是在原来的 js constructoer function 的基础上创建一个新的 js constructor function。所以上面的 NewClass
没有 bar
方法,但是他实例化出来的对象有。
这看起来更像是直接继承,但是考虑到 uiClass
的实现细节,可能有些人就觉得叫 mixin 挺合适的。
下面我们要切换一个话题。
Magento 2 RequireJS Mixins
Magento 2 的 requirejs-config.js
文件有一个 mixin
用法。mixin
这个名字跟我们编程语言中的 mixin 的内涵没啥关系,暂时把他理解成一个名字就好。
虽然我们一直在吐槽这个名字取得不好,但是这功能本身还是很好很重要的哦。Magento 2 RequireJS mixins 让你监听任意 RequireJS module 的实例化,并且让你在实例化返回对象前可以修改他。
下面实验一下,首先我们弄一个空的模块来做实验。
可以通过 alan 的工具 pestle commands 来创建一个空模块
$ pestle.phar generate_module Pulsestorm RequireJsRewrite 0.0.1
$ php bin/magento module:enable Pulsestorm_RequireJsRewrite
$ php bin/magento setup:upgrade
如果你想手动创建模块的话,看这里 Magento 2 简介 —— 不再是 MVC
下面创建一个 requirejs-config.js
文件
//File: app/code/Pulsestorm/RequireJsRewrite/view/base/requirejs-config.js
var config = {
'config':{
'mixins': {
'Magento_Customer/js/view/customer': {
'Pulsestorm_RequireJsRewrite/hook':true
}
}
}
};
然后创建下面的
//File: app/code/Pulsestorm/RequireJsRewrite/view/base/web/hook.js
define([], function(){
'use strict';
console.log("Called this Hook.");
return function(targetModule){
targetModule.crazyPropertyAddedHere = 'yes';
return targetModule;
};
});
下面打开 Magento 首页,你会在浏览器 console 中看到:
Called this Hook
在浏览器的 console 中我们来看看这个模块 Magento_Customer/js/view/customer
,我们会发现这个家伙已经有了一个新的属性 crazyPropertyAddedHere
> module = requirejs('Magento_Customer/js/view/customer');
> console.log(module.crazyPropertyAddedHere)
"yes"
也就是说上面的代码,我们修改了 Magento_Customer/js/view/customer
返回的对象。
如果合理使用的话,这个功能可是非常强大呢。
What Just Happened?
//File: app/code/Pulsestorm/RequireJsRewrite/view/base/requirejs-config.js
var config = {
'config':{
'mixins': {
'Magento_Customer/js/view/customer': {
'Pulsestorm_RequireJsRewrite/hook.js':true
}
}
}
};
requirejs-config.js
文件让每个 Magento module 都可以给 RequireJS 加配置。可以查看这里 Magento 2 and RequireJS
mixins
这个配置不是 RequireJS 里面的,这是 Magento 引入的。这一段就是告诉 Magento ,请你监听 Magento_Customer/js/view/customer
模块,然后让 Pulsestorm_RequireJsRewrite/hook
干活。
然后我们就定义了 Pulsestorm_RequireJsRewrite/hook
模块
//File: app/code/Pulsestorm/RequireJsRewrite/view/base/web/hook.js
define([], function(){
'use strict';
console.log("Called this Hook.");
return function(targetModule){
targetModule.crazyPropertyAddedHere = 'yes';
return targetModule;
};
});
如果你不清楚 RequireJS 模块名字和 URL 之间的关系,请阅读 Magento 2 for PHP MVC Developers 系列
“listener/hook” 的模块都是标准的 RequireJS 模块。他们需要返回一个 callable object (比如 js 的 function)。上面返回的 function 就是模块 loading 后要执行的。他有一个参数 (targetModule),这个参数代表 Magento_Customer/js/view/customer
返回的东东。
不管咱们的 Pulsestorm_RequireJsRewrite/hook
返回什么,它都被当作跟原来一样的东东。这就是为啥 Magento_Customer/js/view/customer
拥有了 crazyPropertyAddedHere
属性。
Class Rewrites for Javascript
上面我们增加了新的属性,我们还可以替换掉原来模块的方法。
define([], function(){
'use strict';
console.log("Called this Hook.");
return function(targetModule){
targetModule.someMethod = function(){
//replacement for `someMethod
}
return targetModule;
};
});
如果我们监听的模块返回的是基于 uiClass
的对象呢?你可以用 uiClass
的 extend
方法,但是用 uiClass
的 _super()
调用其父类的方法。
define([], function(){
'use strict';
console.log("Called this Hook.");
return function(targetModule){
//if targetModule is a uiClass based object
return targetModule.extend({
someMethod:function()
{
var result = this._super(); //call parent method
//do your new stuff
return result;
}
});
};
});
这个功能很强大,但是跟 Magento 1 的 class rewrite 和 Magento 2 的 <preference>
一样,这个是“赢家通吃”的。比如多个开发修改同一个 function ,那么最终只有一个人的代码生效。
好在 Magento 应对这种情况还是有解决办法的,那就是使用 mage/utils/wrapper
Wrapping Function Calls
mage/utils/wrapper
类似 Magento 2 backend around plugin
下面举个例子:
var example = {};
example.foo = function (){
console.log("Called foo");
}
var wrapper = requirejs('mage/utils/wrapper');
var wrappedFunction = wrapper.wrap(example.foo, function(originalFunction){ console.log("Before");
originalFunction();
console.log("After");
});
//call wrapped function
wrappedFunction();
//change method definition to use wrapped function
example.foo = wrappedFunction;
如果你找个 magento 的站点实验下,那么我们可以看到下面这样的输出:
Before
Called foo
After
wrap
方法接受两个参数。第一个是原来的 function (拿饺子打比方,就是饺子馅),第二个参数是你要增加的东西(饺子皮)。originalFunction
指代 example.foo 。
wrapper 模块的作用是不用改原有的代码,就把一个已有的 function 和新的代码包在一起。
wrapper 带来的好处还有,如果好几个人 wrap 同一段代码,那么他们都是有效的,而不会出现像上面那样 “赢家通吃” 的局面。
var wrappedFunction2 = wrapper.wrap(wrappedFunction, function(originalFunction){
console.log("Before 2");
originalFunction();
console.log("After 2");
});
wrappedFunction2();
结果如下:
Before 2
VM502:9 Before
VM502:3 Called foo
VM502:11 After
VM545:4 After 2
所以上面我们“赢家通吃”的代码可以改进为:
define(['mage/utils/wrapper'], function(wrapper){
'use strict';
console.log("Called this Hook.");
return function(targetModule){
var newFunction = targetModule.someFunction;
var newFunction = wrapper.wrap(newFunction, function(original){
//do extra stuff
//call original method
var result = original();
//do extra stuff
//return original value
return result;
});
targetModule.someFunction = newFunction;
return targetModule;
};
});
Why Call this a Mixin
不纠结,不翻译,嘿嘿。。
相关参考
Magento 2’s Base Javascript Class
Magento 2: Simplest UI Knockout Component