利用Marionette Behaviors使你的代码更加干净整洁

本文略译自《Staying DRY with Marionette Behaviors

Backbone 提供的构件过于“简陋”,开发者普遍抱怨Backbone的一点,就是要写太多重复的代码(boilerplate)——不断写UI事件逻辑代码,业务控制逻辑代码。

削减boilerplate code,是Marionette的目标之一。而在之前的「高级V基类」的基础之上,Marionette还提供一个通用基类——the Behavior class,进一步削减重复的代码,让你的代码更加的DRY。

EM:V的代码复杂到居然可以「再度的抽象」

EM:理解和应用Behavior的前提,是对V有深刻全面的了解

Marionette Behaviors 类是对「通用的UI逻辑代码」的抽象,故它可“植入”你的View中,实现复用。当你发现在多个V类中出现类似的行为代码,那么你可用Behaviors对它进行抽象,来降低boilerplate。

在开始介绍Behaviors API前,我们先看一个实例。

#表单验证的代码

我们来看一个使用Behaviors类改进表单验证代码的例子。

表单验证我常用backbone.validation 库。它可检测model属性的合法性,并可在控件上给出提示信息,而且不限定验证流程。不过,我习惯使用如下的一个验证流程。

  • 第一,用户可在他们认为合适的时候随意输入数据,在他们提交表单之前,没有验证警告;
  • 第二,在他们最初提交表单之后,任何无效字段旁边都会出现警告提示,当用户修复了字段时,提示信息应该更新并按需消失。

使用backbone.validation很容易实现这个验证流程。我们只需要简单的将 Model 连接到(我创建的每个)表单View,并在 Model 中添加一些逻辑,以便在每次 Model 更改时重新验证,但仅在用户submit触发第一次验证之后。

以上是backbone.validation在大多数情况下的使用方式,然而当我们需要在这个验证过程中重新渲染(re-render )表单视图时,则需要一额外的处理。因为当表单有子视图时(它可能依赖于表单其他部分的内容),子视图有可能会重新渲染(这情况是常见的),造成验证失效。这种情况下,我们得保证重渲染前后验证状态(validation state)的一致性,下面是一个例子:

import * as Mn from 'marionette';
import * as Validation from 'backbone-validation';

var FormView = Mn.LayoutView.extend({

    template: '#form',
    
    ui: {
        submit: '.submit'
    },
    
    events: {
        'click @ui.submit': 'submitForm'
    },
    
    modelEvents: {
        'validated': 'setValidated',
    },
    
    onRender: function() {
        //any other post-render View code here
    
        Validation.bind(this);
        if(this.validated) {
            this.model.validate();
        }
    },
    
    setValidated: function() {
        this.validated = true;
    },
    
    submitForm: function() {
        //handle form submission
    }

});

额外的验证代码并不多,但是如果有多个类似的表单需维护,则需要考虑维护成本。这种情况,Behaviors 怎么用呢?Behaviors能帮我将通用的部分提取出来(有点函数的提取),类似这样:

import * as Mn from 'marionette';
import * as Validation from 'backbone-validation';

var ValidationBehavior = Mn.Behavior.extend({

    modelEvents: {
        'validated': 'setValidated',
    },
    onRender: function() {
        //Set up any other form related stuff here
        Validation.bind(this.view);
        if(this.hasBeenValidated) {
            this.view.model.validate();
        }
    },
    
    setValidated: function() {
        this.hasBeenValidated = true;
    },

});
export default ValidationBehavior;

然后,你可以返回表单View对这个 Behavior进行“调用”:

import \* as Mn from 'marionette';
import \* as Validation from 'backbone-validation';
import \* as ValidationBehavior from 'behaviors/validation';

var FormView = Mn.LayoutView.extend({

    template: '#form',
    
    ui: {
        submit: '.submit'
    },
    
    events: {
        'click @ui.submit': 'submitForm'
    },
    
    behaviors: {
        validation: {
            behaviorClass: ValidationBehavior
        }
    },
    
    submitForm: function() {
        //handle form submission
    }
});

同样同理,你可以对任何「UI交互功能」进行抽象,只要是通用的,诸如热键绑定(key-binding)和“删除前先警告”。

Behaviors API

Behaviors 类本质只是一个 View 的片断,所以View的所有API 都适用于Behaviors。例如event hashes, ui elements,和 life-cycle methods。

Behavior 类的「生命事件勾子」会在它所属的View的同一个勾子函数运行后立即被执行。而 Behaviors的el, $el 和 $ 属性则它的View同样属性的代理而已。

不过,Behaviors不只是一种mixins,它有自己的一些独立性(有私有属性),这保证它能通用于多个不同的View。例如,Behaviors 一般不会「直接」修改所属View的数据(这样B就不依赖V),通信是单向的,View通过创建时的 options object将数据传给Behaviors,例如上面例子中,将提示信息(errorMessage)传给 ValidationBehavior:

behaviors: {
    validation: {
        behaviorClass: ValidationBehavior,
        errorMessage: 'You did something wrong.'
    }
},

#Behaviors API的一些注意事项

从上面的例子里,我们看到了 Behaviors的API——View “调用”behaviors是一种 behaviors hash的声明定义的方式,一个 behavior 一个键值对。而有人可能注意到“值”是一个options object,“键”名则是无关的,Marionette判断behavior的标识是behaviorClass 的option值。API为什么是这样设计的呢?要理解这种设计,必须知道Behaviors API 原来支持两种behavior查找方式。

最初Behaviors的API需要你重载一个 behaviorsLookup 函数,behaviors hash 的key是用作查找的标识;这种方式很适用只使用「全局命名空间」来组织代码的项目,而后来对于使用modern JavaScript module loaders组织代码时,引入一个全局状态和间接引用变得多余,为了兼容所以加一个behaviorClass 的option值,这样开发者可以直接将 behaviors 指定为View文件的依赖项。

使用两个标识显得API有点笨重,为了简化API ,幸好新版Marionette支持将behaviors定义成数组,一个behavior一个数据项,上面例子可这样写:

behaviors: [{
    behaviorClass: ValidationBehavior,
    errorMessage: 'You did something wrong.'
}]

#Behaviors何时最有用

Marionette官方文档对Behaviors的解释很宽泛,这是官方文档一贯的风格,为的是不限制用户对工具的使用。不过,据我的经验,Behaviors主要适用于以下三种情况。

第一,对UI事件处理逻辑的抽象;

对复杂的UI交互行为进行抽象,是Behaviors设计的主要目标,这个工具的主要任务。UI交互行为模式诸如“关掉前警示”,是非常常见的交互现象,所以值得抽象重用。除这种简单的交互行为,behaviors还可联合在一起实现对一些复杂的行为的抽象,例如“拖放”(drag and drop)功能——CollectionView负责实现Droppable Behavior ,ItemViews实现Draggable Behavior 。

第二,对V的生命事件勾子的抽象;

其实Behaviors可以对 View 任何形式的部分(定义View对象所有代码)进行抽象,所以除了UI交互处理代码,Behaviors也可以View生命事件勾子函数进行抽象。

第三,简化对第三方库的集成

这一类别任务是第二种的一种特例,我觉得有些特别,值得另立一类别。很多第三方的Backbone 或 jQuery 的库插件使用时需要在View上做不断重复的初始化代码。这些相同的代码通常会在多个Views重复的出现,最多也是做一些微调,这种情况非常适合应用Behavior。上面那个「表单验证」的例子就是其中之一。 这种需要初始化的插件是很多的,再举一个Chosen jQuery plugin;Chosen可以用来创建非常友好的下拉列表(看官方例子)。你可用Behavior将 Chosen 的初始选择项的代码抽象出来,从中你也可以用一个条件判断将特定 css class的外的选择项过滤掉:

let UseChosen = Marionette.Behavior.extend({
    onRender: function() {
        let className = this.options.className,
            chosenOptions = this.options.chosenOptions || {};
        if(className) {
            this.$('.${className}').chosen(chosenOptions);
        }
        else {
            this.$('select').chosen(chosenOptions);
        }
    }
});

参考

裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.