The Curious Case of Magento 2 Mixins (翻译)

原文地址

这篇要说的是 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 barfoo 方法。

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 的对象呢?你可以用 uiClassextend 方法,但是用 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

发表评论

电子邮件地址不会被公开。 必填项已用*标注