Skip to content

Instantly share code, notes, and snippets.

@watert
Last active August 29, 2015 14:06
Show Gist options
  • Save watert/a2b656d8e1a67daefd04 to your computer and use it in GitHub Desktop.
Save watert/a2b656d8e1a67daefd04 to your computer and use it in GitHub Desktop.
蛋疼的前端面向对象

提到 JavaScript 面向对象编程,似乎书籍里说的都是原型链与闭包,稍微高端一点的话说的会是函数式编程。

但我想真正做过这类事的童鞋们,会发现实际工程项目中,在这些基础上构建出来的代码,维护起来会相当蛋疼。

绕来绕去各种各样的闭包、原型链,把函数一层一层地传来传去,各种事件弄得到处都是……

那些东西当然都是JS的优势所在,但却使得代码变得相当混乱与难以理解。问题是,JavaScript并没有从语法层面去解决这些问题,所以我们都得自己想办法。

其实前端编程,从应用层、与实际的问题角度来看,处理起来无非也就那些东西:

  • Router处理页面的位置,过去、现在与将来;
  • Controller处理大体的逻辑
  • View封装起具体模块的逻辑
  • Model封装数据模型
  • 各种各样的细节

反观现在大家用的主流的框架,Backbone / Angular / Emberjs ,其实都有不断地重复做一件事:就是构建起自己的对象与继承模型。

其实恰说明了这个所有其它语言都有从语法层面提供的功能,其实相当重要、关键而基础,尽管相当虽然看起来不那么像JS做的事。

比如Backbone的风格:

var NewView = Backbone.View.extend({
    initialize:(){
        /* Initialize */
    }
});

比如 CoffeeScript的风格:

class NewView extends View
    initialize:()->
        ### Initialize ###

关键点在哪里?

关键在于这种类与继承风格的语法所解决的问题:

在之前简单的模型之上,一层一层地构建出更复杂的模型,进而组成一个复杂的系统。

这当然与UNIX的管道风格不一样,但在很多应用层开发的情景下却显得非常实用实在。因为业务往往就是这样的:不是单一化的完美产品,而是各种各样定制化的修改。

想要不那么蛋疼,就试着学着使用那些工具吧。

粗略地与翻译自 Smashing Magazine:

在上一个部分,我们主要讨论了Marionette的Application。而这次,我们讨论下Backbone.Marionette中包含的模块系统。Marionette的模块可以通过Application来访问,不过这部分内容是个相当大的话题,值得用一篇文章来讨论。

模块是什么?

在讨论如何使用Marionette的模块之前,得先确保我们有一个较为得体的对模块的定义。模块是一个独立的代码单元,理想情况下只完成一件事情。他可以与其它模块互动来完成一个完整的系统。一个模块越独立,他越能被替换、或是进行一些内部改造,而不影响到系统的其它部分。

对于这篇文章来说,我们只讨论到我们的需要来定义模块,不过如果你需要更多关于编写模块化的代码的相关内容,互联网上可以找到大量的资料,《深入单页面应用》中的可维护性依赖于模块化章节 就是篇不错的文章 。

JS语言现在还没有任何内建的方式来进行模块管理(新版本的ECMA似乎会进行相关支持),但许多正在兴起的工具库都能解决这类定义和加载模块的问题。Marionette的模块库,非常遗憾地没有提供从外部文件加载相关库文件的功能,但他也提供了很多其它模块工具库所没有的功能,比如启动和停止一个模块。我们之后会谈到具体的相关内容,但现在我们先看看如何定义模块。

模块定义

让我们从最基本的模块定义开始。像之前说到的,模块可以通过Application对象访问到,所以我们需要先实例化其中的一个Application,然后我们可以使用module方法进行模块的定义。

var App = new Backbone.Marionette.Application();
var myModule = App.module(‘myModule’);

非常简单对么,嗯,这就是我们能定义的最简单的模块。而我们到底创建了什么?本质上,我们告诉了Application我们想要一个空壳模块,它上面没有添加任何实际的方法或功能,而且这个模块的名字叫myModule(取决于我们传给module方法参数的内容)。但什么是空壳模块?他是Marionette.Module类的一个实例。

Module 类有着一些内建的功能,比如事件功能(通过EventAggregator,我们在上一篇文章中有讨论到),通过初始化方法(Initializers)来开始、通过终结方法(Finalizers)进行停止(我们会在“启动与终止模块”部分深入讨论)。

标准的模块定义

现在让我们看看如何定义一个有些我们自己的功能的模块。

STANDARD MODULE DEFINITION Now let’s look at how to define a module with some of our own functionality.

App.module("myModule", function(myModule, App, Backbone, Marionette, $, _){
    // 私有数据及方法
    // Private Data And Functions
    var privateData = "this is private data";

    var privateFunction = function(){
        console.log(privateData);
    }

    // Public Data And Functions
    // 公有数据及方法
    myModule.someData = "public data";

    myModule.someFunction = function(){
        privateFunction();
        console.log(myModule.someData);
    }
});

就像你看到的,这里有很多我们自己的东西。让我们从最上面一行开始看起。就像之前说的,我们调用App.module并提供一个名称给这个模块。但现在,同时我们也传入了一个函数。这个函数传入了好几个参数。我打赌你能弄明白他们是什么,通过我给这些参数的命名,但我依然会说明所有的内容:

  • myModule就是那个你在尝试创建的模块。记住,他已经被你创建,然后他是一个新的Module的实例。你可能即将想要扩展它,添加一些新的忏悔或方法;又或者,你也许想保持简单的语法而不需要你在函数中传入(有点没明白这句在说什么:otherwise, you might as well stick with the short syntax that doesn’t require you to pass in a function.)。
  • App就是那个你调用模块的Application对象。
  • Backbone,当然,就是Backbone库的引用。
  • Marionette就是Backbone.Marionette库的引用。它实际上可以通过Backbone来访问,但这样做可以让你通过一个更短的别名进行访问。
  • $就是你的DOM工具库,通常我们使用jQueryZepto
  • _UnderscoreLodash的引用,取决于你实际使用的是哪个。

通常情况下,我会说大多数的这些参数都是不必要的;毕竟,为什么你不会已经访问了所有的这些引用?但是,我可以看到在下面几种情况下这是相当有用的:

  • 一个对这些参数的最短的名称,可以节省一些字节的流量;
  • 如果你正在使用RequireJS或其它的模块加载器,你只需要加载Application对象作为依赖。而其它都可以通过这些Module的参数进行访问。

不管怎样,让我们回过头来说明下上面剩下的代码都在做什么。在这个方法里面,你可以利用闭包来创建私有的变量或函数,像上面我们已经做了的那样;你也可以公有地暴露一些数据或函数,通过把他们作为属性添加到myModule上去。这就是我们创建和扩展模块的方法。这个函数没有必要返回任何东西,因为模块将会直接能够通过App进行访问,我将会在下面的“访问一个模块”章节对进行解释

注意:确保你只是尝试添加属性到你的模块变量而且不要将模块赋值为什么东西(比如,myModule={...}),因为如果你把模块变量赋值为其它东西,会改变这个变量的引用,然后你所作的任何改变都不会之后在你的模块上体现出来。

在之前,我注意到你可以传入一些自定义的参数。事实上,你可以传入做任意多的参数只要你需要。看看下面这些代码是怎么做的:

App.module("myModule", function(myModule, App, Backbone, Marionette, $, _, customArg1, customArg2){
    // Create Your Module
}, customArg1, customArg2);

就像你看到的,如果你传入了多余的参数到module,它会把那些参数传入这个你定义模块时的函数中。再一次地,最大的好处在于我看到这可以在最小化命名后节省一些字节量,而除了这个,我没看出更大的价值。

另一点需要注意的是,this关键字也能在这个函数中使用,并且它实际指向的是这个模块自身。这意味着你甚至不需要第一个参数,但你同时也会失去那些最小化参数带来的优势。让我们通过this关键字重写这些代码,然后你会看到这实际跟上面的myModule是等效的。

App.module("myModule", function(){
    // Private Data And Functions
    var privateData = "this is private data";

    var privateFunction = function(){
        console.log(privateData);
    }

    // Public Data And Functions
    this.someData = "public data";

    this.someFunction = function(){
        privateFunction();
        console.log(this.someData);
    }
});

就像你看到的,因为我没有使用任何的参数,我决定不去使用上面所使用的那些库。非常明显你也可以直接略过那第一个参数,而直接使用this引用。

分离定义

而在定义模块方面我最后会讲一下我们能把不同的定义分离开来。我不知道这样做会有什么用,但有些人也许想事后再进行模块的扩展,所以分离开定义也许能帮他们避免触碰到你原有的代码。下面就是一个分离定义的例子:

// File 1
App.module("myModule", function(){
    this.someData = "public data";
});

// File 2
App.module("myModule", function(){
    // Private Data And Functions
    var privateData = "this is private data";

    var privateFunction = function(){
        console.log(privateData);
    }

    this.someFunction = function(){
        privateFunction();
        console.log(this.someData);
    }
});

这给了我们像之前的定义一样结果,但他分离开变成两次的定义。这能够运转因为在File 2这第二个文件中,在File 1中定义的模块被传入给了我们(假设File 1File 2之前加载)。当然,如果你尝试访问一个私有的变量或方法,它必须被定义在当前的模块中,因为它只能在当前的闭包中进行访问。

访问一个模块

这很搞笑如果我们创建了一个模块又不能访问他们。我们需要访问他们从而使用他们。嗯,在这篇文章最开头的代码片段中,你看到我调用module时,我将他的返回值赋值给了一个变量,这是因为我使用非常简单的方法来同时定义和获取模块。

var myModule = App.module("myModule");

通常,如果你只是尝试获取这个模块,你会传入第一个参数,module 方法就会出去寻找到那个你需要的模块。但如果你传入了一个函数作为第二个参数,这个模块就会添加那些你的功能,然后它依然会返回你创建的或更改了的的模块。这意味着你能通过同一个方法定义你的模块并获取它。

这并不是唯一的方式进行模块的获取。当一个模块被创建时,它就会直接被附加到构建模块时所调用的Application对象上。这使得你能使用简单的对象属性获取操作来访问你的模块;但这次,这个模块必须已经在之前被定义,否则你会得到一个undefined引用。

// 同样能运作但我不推荐这样使用
var myModule = App.myModule;

虽然这样的语法更为简短,但这对于其它的开发者并没有表达相同的信息。我会更推荐使用module方法来进行模块的访问,所以非常明显地你正在访问一个模块而不是App的其它什么属性。便利与危险在于它会创建这个原来不存在的模块。如果你误拼写了这个模块的名称,你将不会有任何方式知道你并没有获取正确的模块,直到你尝试访问一个不存在的属性。(译者注:这段也翻译得感觉有点怪)

子模块

模块也能够拥有子模块。遗憾的是,Module 没有他自己的module方法,所以你不能直接地给模块添加子模块,但这并不会让我们放弃。取而代之的是,想要创建子模块,就像我们之前做的,你依然调用Appmodule方法;但命名这个模块时,你需要添加一个点(.)到你的父模块名称之后,然后才是子模块的名称。

App.module('myModule.newModule', function(){
    ...
});

通过在模块名称上使用点分割符,Marionette知道了它应该正在创建某个模块的一个子模块。酷(同时也是潜在的危险)的地方是如果父模块并没有创建,它会在子模块这里进行创建。这可能很危险,因为像我之前说的如果你打错了一些字母。你可能创建一个你并没希望创建的模块,并且这个子模块会被附加到这个模块上,而不是你所希望附加的模块上

访问子模块

像之前说的,子模块可以通过定义时同样的方式进行访问,或者你也能通过模块的属性进行访问。

// 这些都能运作,但推荐使用第一种方式
var newModule = App.module('myModule.newModule');
var newModule = App.module('myModule').newModule;
var newModule = App.myModule.newModule;

// 这些不能运作。模块本身并没有一个`module`方法
var newModule = App.myModule.module('newModule');
var newModule = App.module('myModule').module('newModule');

上面所说到的访问子模块的三种方法都能运作并返回一样的结果,只要模块与子模块都已经被创建了。

启动与终止模块

如果你看了之前那篇关于Application的文章 ,你已经知道你能够启动一个Application通过它的start方法。好,启动一个模块是一样的,而且他们也能通过stop方法进行终止。

如果你重新调用(假设你已经阅读了之前的文章),你可以通过addInitializer添加初始化器到Application,然后他们会在它启动时执行(或者如果Application已经启动,它们会立即执行)。在你启动一个Application时,其它的一些事情也会发生。下面是所有会发生的事件,按顺序:

  1. 触发 initialize:before 事件
  2. 启动所有已定义的模块
  3. 执行所有的初始化器,依照他们添加的顺序
  4. 触发 initialize:after 事件
  5. 触发 start 事件

一个模块的表现基本也是这样。事件的数量及一些名称会不同,但大体上还是相同的过程。当一个模块启动时,它会:

  1. 触发 before:start 事件
  2. 启动所有已经定义的子模块
  3. 执行它的所有初始化器,依照他们添加的顺序
  4. 触发 initialize:after 事件
  5. 触发 start 事件

stop方法也非常相似。相对于添加初始化器,你需要添加终结器(finalizers)。你通过addFinalizer方法传入函数来执行进行这个操作,然后这些finalizer就会在stop调用的时候执行。不像初始化器那样,没有数据或参数会传入到每一个函数。当一个stop调用时:

  1. 触发 before:stop 事件
  2. 终止这个模块的所有子模块
  3. 执行所有添加的finalizers
  4. 触发 stop 事件

Initializers和Finalizers并不意味着被其它人使用。事实上,在模块定义内部使用他们相当地有用。通过这种方式,你能定义一个模块却不创建任何使用的对象,但写许多initializer来开始创建一些对象并设置他们,就像下面的例子这样:

App.module("myModule", function(myModule){
    myModule.startWithParent = false;

    var UsefulClass = function() {...}; // 构建函数定义Constructor definition
    UsefulClass.prototype ... // 完成定义UsefulClassFinish defining UsefulClass
    ...

    myModule.addInitializer(function() {
        myModule.useful = new UsefulClass();
        // More setup
    });

    myModule.addFinalizer(function() {
        myModule.useful = null;
        // More tear down
    });
});

自动和手动启动

当一个模块已经定义了,默认地它会在它的父模块启动时自动启动(不管是根Application对象或是一个父模块)。如果一个模块定义在一个已经启动的父模块下,它会立即启动。

你可以在定义函数中通过两种方式之一设置一个不自动启动的模块。在定义函数中,你可以设置模块的startWithParent参数为false,或你可以传入一个对象(而不是一个方法)到模块中并且它有一个startWithParent属性为false和一个define属性来替换原来的定义函数。

// 在函数内设置 'startWithParent'属性
App.module("myModule", function(){
    // 将 'startWithParent' 赋值为false
    this.startWithParent = false;
});

// -- 或 --

// 传入一个对象
App.module("myModule", {
    startWithParent: false,

    define: function(){
        // 在这里定义模块
    }
});

App.start();

// myModule不会启动,所以我们需要手动启动它
App.module('myModule').start("Data that will be passed along");

**现在这个模块不会自动启动。你必须得手动调用start方法来启动它,就像我在示例代码中做的那样。传入到start的数据可能是任何东西或任何类型,然后当他启动时它会单独地传入到子模块中,传入到initializers,以及before:startstart事件 **

像我说的,当你调用stop方法时数据并不会像这样传递。另外,stop必须手动地调用,同时它也同时调用子模块的stop方法;没有任何方式来绕过它。这有意义因为父模块停止运作时子模块不应该继续运行,虽然有些情况下子模块停止了但父模块应该继续运行。

其它的事件及内建的功能

我提到过Module有一些内建的功能,比如事件汇合器 EventAggregator。像讨论地,我们能在module上使用on方法来监听这些与启动与终止相关的事件。这并不是全部内容。没有其它的内建的事件了,但一个模块可以定义与触发它们自己的事件,看看这些代码:

App.module('myModule', function(myModule) {
    myModule.doSomething = function() {
        // Do some stuff
        myModule.trigger('something happened', randomData);
    }
});

现在, 不管什么时候我们在模块上调用doSomething,它都会触发something happened事件,而你可以订阅(监听)这个事件:

App.module('myModule').on('something happened', function(data) {
    // Whatever arguments were passed to `trigger` after the name of the event will show up as arguments to this function
    // Do something with `data`
});

这与我们在Backbone代码中的collection, model或views中处理事件时是非常类似的。(译者注:相似?其实我就没看出有任何区别……)

我们实际上如何使用模块

Marionette中的模块可以以相似地方式像其它模块定义库那样使用,但这并不是它实际上设计的意图。内建的startstop方法就是一个迹象。这些Marionette所包含的模块其实是用于表达一些大型应用的子系统。比如,让我们看看Gmail。

Gmail是一个单应用,但实际上它包含了相当多个更小的应用:Email客户端,聊天客户端,电话客户端及联络人客户端。每一个都是独立的——它能独立地存在——但他们都存在于同一个应用中并且可以与其它应用交互。当我们第一次启动Gmail,联络人管理器并没有启动,聊天窗口也没有启动。如果我们需要在Marionette应用中表达这些,每一个子应用都应该是一个模块。当一个用户点击一个按钮来打开联络人管理器时,我们可能停止Email应用(因为它变得隐藏起来了——虽然,为速度考虑,我们应该让之继续运行,只是让他不在DOM节点中显示),同时启动联络人管理器。

另一个例子就是,一个大量基于widget(小组件)的应用。每一个widget都会是一个模块,你能通过启动或停止来进行显示或隐藏它。这会更像是一个可定制的仪表盘(Dashboard)比如iGoogle,或者是WordPress的管理平台。

结论

噢,这真是大量的信息。如果你已经看到了这里,我赞赏你(虽然你读这些文章已经比我写这篇文章轻易太多)(译者注:我作为翻译应该夹在中间……)。不管怎样,我希望你已经学习了大量关于Marionette如何处理定义、访问、启动及终止模块和子模块的知识。你可能发现这是一个非常好用的工具,或也许你会选择完全无视它的存在(译者吐嘈:我之前就是这么做的)。这就是一个Backbone及Marionette非常棒的地方:大量他们的功能都是相对独立的,所以你能挑选你想要的来使用。

译者总结

这篇文章真是好长好长好长,我简直都要翻译哭了 T-T 本来觉得这篇是比较没那么干货的一篇文章,因为模块系统这种玩意基本上是比较被玩烂了的东西,而且也有着更优化的解决方案,比如requirejs或seajs等等;但翻译下来发现,Marionette中的模块系统的设计还是相当有意思的。

他其实设计的方向是与别的模块系统非常异质化的,处理了很多从业务层面考虑才会有的问题,比如子模块、通用的事件触发、多次定义等等。尤其是其中的多次定义功能,在文章里说是 Split Definition,但在程序员角度看来,这就是针对切片编程的一个概念,做过JAVA开发的童鞋应该会感到非常的怀念。就像文中所说的,比起module,他更像是sub-app的概念,解决的也是类似的场景。

但明显地,他也有着他的不足:明显地,他说这个应用适合于Dashboard类型的应用,而我却觉得不见得——我似乎没看到创建一个模块的N个实例的可能性,似乎也不可能定义一个匿名模块,更别谈匿名模块的子模块了——至少在他原先的设计上是不支持的。所以,他更多像是某种单例模式的应用实现,仅此而已。

关于这个系列文章

我本来以为这个系列文章只有两篇,但结果发现在Smashing Magazine里还有第三篇: Part 3,主要内容说的是Marionette如何帮助Backbone.View变得更好(how Marionette helps make views better in Backbone)。这似乎也是我最开始尝试研究与使用Marionette的原因,算是比较干货的东西了。这篇,我之后会抽空再进行翻译。

而在这三篇文章之外,还有Smashing Magazine的付费电子书《Better Backbone Applications with MarionetteJS》,几乎覆盖了Marionette的所有内容,似乎还是相当有价值的。但因为版权问题我估计就不会继续翻译下去了。

粗略地与翻译自 Smashing Magazine:

Backbonejs正在变得越来越流行,但不像Emberjs(译者按:Angularjs也是),Backbone因为他的最小化,使得大量的问题留给了开发者需要自己想办法解决。

所以,一旦你介入到了一个更复杂的应用,Backbone就变得不那么简单了(虽然这本来是他的优点)。Marionette就是来解决这种Backbone开发的、逐渐出现的痛苦。

中心App对象

大多数人做一个Backbone App时都会先整一个中心对象并把所有的东西绑在上面,通常命名为App或Application。但Backbone本身不提供这个功能,所以很多人直接使用Router作为这个对象。当然有App对象总比没有的强,但Router却不是设计来做这类事的。

Derick Bailey,Marionettejs的作者有一个更好的解决方案:他创建了一个Class来专门做这类事。你可以这样创建一个App: var App = new Backbone.Marionette.Application(),然后,当一切就绪时,这样启动App:App.start(options)。我们之后会讨论那个options是用来做啥的。

初始化器 Initializers

Marionette.Application 最酷的地方就是Initializers。当你的代码是模块化时,应用启动时会有许多东西需要初始化。比起把一切初始化过程都放在main.js里面,你可以把初始化的过程直接放到Module文件里(译者按:这样做,似乎不太好吧……),像这样:

var SomeModule = function(o){
  // SomeModule的创建器
};

App.addInitializer(function(options) {
  App.someModule = new SomeModule(options);
});

所有通过这种方式添加的Initializer都会在App.start被调用的时候运行。注意,options参数会被传入每一个initializer方法,这就是你在调用App.start(options);时所传入的对象。这是传入处理不同的应用配置的绝佳方法,并且所有的模块都能使用它。

一些事件也会在这些Initializer调用过程中被触发:

initialize:before:在所有Initializer调用前触发; initialize:after: 在所有Initializer都完成之后触发; start:在initialize:after之后触发。

你可以监听这些事件进行更多的控制,比如:

App.on('initialize:before', function(options) {
  options.anotherThing = true; // Add more data to your options
});
App.on('initialize:after', function(options) {
  console.log('Initialization Finished');
});
App.on('start', function(options) {
  Backbone.history.start(); // Great time to do this
});

非常简单,却可以增加你的应用的灵活性。

事件汇合器 Event Aggregator

Application对象还通过使用事件汇合器给予Backbone App更多的解耦的可能性。之前我写过一篇文章Scalable Javascript Applications,里面我有提过,系统中的每一个模块都应该完全独立于其它模块,然后他们能通信于其它模块的唯一方法就是通过全局对象的事件。通过这种方式,一个模块可以监听他们需要监听的事件、并回应这些事件,而不需要了解系统外的其它东西甚至是否存在。

这是一个好的开始,而且这个功能同样适用于一些小应用。

事件汇集器可以通过App的属性vent调用,你可以简单地订阅或退订(译注:亦即监听与停止监听)通过onoff方法。这些方法听起来相当熟悉,因为这个事件汇集器就是扩展自Backbone的Event对象。你需要担心的唯一问题是我们正在一个任何地方都能访问到地对象上使用事件,所以所有的模块都能通过它来通信。

区域 Regions

Region是另一个Marionette的模块使得你可以轻松地绑定View到不同的HTML的区域。 我不会讲具体的Region的工作方式——那是另外一个一整天的课题——不过我会简单地说明一下并说明如何与Application协同使用它们。

一个Region是一个对象,通常通过这种方式创建:new Backbone.Marionette.Region({ el: 'selector'})——它可以管理一个你绑定了一个视图(View)的区域。你可以添加一个视图并通过show方法自动地渲染它。然后,你可以关闭这个视图(意味着移除相关的DOM节点,并且如果你用了Marionette自己的视图的话还会自动解绑所有的事件(译注:似乎Backbone的后续版本也会在相应的remove方法中自动处理所有的事件解绑)),并且通过再次调用show方法渲染一个不同的视图。Region当然可以做更复杂的事情,但事实上能通过一个方法就处理视图的渲染与关闭使这些Region非常实用。来让我们用Code说话:

// 创建一个region,它会控制#container里面的DOM
var region = new Backbone.Marionette.Region({
  el: "#container"
});

// 添加一个视图到region里。这个视图也会立即自动渲染。
region.show(new MyView());

// 关闭现在的这个视图,并渲染显示另一个视图
region.show(new MyOtherView());

// 关闭这个视图,使得#container什么都不显示
region.close();

如果你想通过App直接访问某些Region(类似App.someRegion),有一个简单的方式可以直接添加:addRegions。你可以通过三种不同的方式使用addRegions,每种都会直接把Region通过参数名添加到App对象上,但每种方式使用的值都不一样,取决于你希望通过哪种方式完成这个操作:

选择器 Selector:

简单地提供选择器字符串,然后一个标准的region就会被通过传入选择器字符串到创建的新的region的el参数里:

App.addRegions({
  container: "#container",
  footer:    "#footer"
});

// 等同于:
App.container = new Backbone.Marionette.Region({el:"#container"});
App.footer    = new Backbone.Marionette.Region({el:"#footer"});

自定义Region类型:

你可以继承Region来创建你自己的region类型。如果你想要用你自己的region类型, 你可以用下述的语法。注意,使用这种方式,el必须已经定义在你自己的Region类型里:

var ContainerRegion = Backbone.Marionette.Region.extend({
  el: "#container", // Must be defined for this syntax
  // Whatever other custom stuff you want
});

var FooterRegion = Backbone.Marionette.Region.extend({
  el: "#footer", // Must be defined for this syntax
  // Whatever other custom stuff you want
});

// Use these new Region types on App.
App.addRegions({
  container: ContainerRegion,
  footer:    FooterRegion
});

// This is equivalent to:
App.container = new ContainerRegion();
App.footer    = new FooterRegion();

自定义Region类型并使用选择器:

如果你没有定义el,或者你想要在你自己的Region类型中改写它,你可以使用这样的语法:

var ContainerRegion = Backbone.Marionette.Region.extend({});

var FooterRegion = Backbone.Marionette.Region.extend({});

// Use these new Region types on App.
App.addRegions({
  container: {
    regionType: ContainerRegion,
    selector:   "#container"
  },
  footer: {
    regionType: FooterRegion,
    selector:   "#footer"
  }
});

// This is equivalent to:
App.container = new ContainerRegion({el:"#container"});
App.footer    = new FooterRegion({el:"#footer"});

你可以看到,添加全局App范围的region非常的简单(尤其是使用的是标准Region类型的时候),而且它们添加了很多有用的功能。

总结

就像你见到的,Marionette添加了一堆很棒的功能使得Backbone开发变得更简单,而且我们目前还只说明了它提供的很少的模块(当然,我们已经使用了很多Application自己用到的模块,当然那些需要更多的研究)。我希望这会诱惑你渴望阅读更多的后续内容。

译者总结

总地来说Marionette提供了很多放眼整个应用框架级别的设计思路与工具,而这些是每一个复杂应用都无法绕过的,所以能减少很多重复制造的轮子。这部分的东西恰是Emberjs或Angularjs的优势,也是Backbone所没有涉及到的内容。

后续的Part 2还没翻译,看这篇文章的反响如何先吧。

README

APIs:

app.query({"key":"value","key2":"value2"}) // 基于当前 URL 设置参数
app.route("analyze/overview") // 直接设置 URL 进行跳转。会带上默认参数。
app.params() //获取当前Query参数列表
var url = App.params({tab:"asdf"}).get("url") //获取增量修改某个参数后的url

Views:

  • <div data-view="globalQuery"></div> 产品、设备及日期导航
  • <div data-view="query.sources"></div> 渠道导航
  • TODO: 预计添加图表View

图表 Helper

charts.load "trend/funnel",(coll)=>
	@$(".chart-rate").fastChart
		collection:coll
		model:"funnel"
		parser:"bubble"
		fields:[{y:"register_rate",z:"total_num_event"}]
		chartConfig:
			title: text: "Register Rate"
			yAxis: title: text: "Register"

代码继承关系

| Backbone.View
| - app/baseView // 处理Render方法及renderSubViews方法
  | - views/* 	// 用于tag化调用的零散视图,
  |			  	//如使用这种方式调用通用导航: <div data-view="globalQuery"></div>
  | - app/router // RouterController, 封装了Query及Route等通用方法,并管理了顶级routes视图
  | - app/baseRoute // 单个的Route视图,可以理解为ViewControllers
  	| - routes/overview
  	| - routes/trend
  	| - routes/tracking
  	| - routes/roi
  	| - routes/tracking
  	| - // 这一层级的都是顶级view controllers,直接由路由托管加载

RouteView 写法:

  1. routes/目录下创建 [routeName].js文件
  2. 按照requirejs规范调用app/baseRoute并继承它
  3. 写相应的tmpl, onRender, onShow等属性或方法
  4. 如果有tabs, 务必在tmpl里包含相应的HTML,并写 charts 参数传入各tab的chart参数

example:

var data=App.billData();JSON.stringify(data,null,"\t")
"{
	"fill-degree": [
		{
			"glassId": "14123223451402403306",
			"degreeId": "14123223451402403306"
		},
		{
			"glassId": "14123223451402403306",
			"degreeId": "14123223451402403306"
		},
		{
			"glassId": "14123223451402403306",
			"degreeId": "14123223451402403306"
		}
	],
	"fill-addr": {
		"id": 3,
		"province": "广东省",
		"city": "深圳市",
		"area": "南山区",
		"detail": "世界之窗2034号",
		"consignee": "Zem",
		"phone": "15681548526"
	},
	"receive-time": {
		"packageType": "multiple",
		"deliver-time": "all"
	},
	"payment": {
		"payment": "alipay"
	},
	"invoice": {
		"invoice": "electric",
		"invoice-title": ""
	},
	"submit": {}
}"

作为对于做页面重构或是纯前端出身的童鞋,Angular其实对于我来说非常吊诡:他绑架了整个HTML与JS,使得我必须依照他的方式来进行开发。

但另一方面,我又眼睁睁地看着他社区不断扩大、不断完善,越来越多的插件……似乎他已经变得势不可挡。

于是,我觉得,也许我也该开放些地看待Angular,去学习Angular,虽然,不一定使用Angular。

感觉别的Angular的资料很多都是在白费功夫,就不推荐了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment