在上一个部分,我们主要讨论了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工具库,通常我们使用jQuery或Zepto。
- _是Underscore或Lodash的引用,取决于你实际使用的是哪个。
通常情况下,我会说大多数的这些参数都是不必要的;毕竟,为什么你不会已经访问了所有的这些引用?但是,我可以看到在下面几种情况下这是相当有用的:
- 一个对这些参数的最短的名称,可以节省一些字节的流量;
- 如果你正在使用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 1在File 2之前加载)。当然,如果你尝试访问一个私有的变量或方法,它必须被定义在当前的模块中,因为它只能在当前的闭包中进行访问。
访问一个模块
这很搞笑如果我们创建了一个模块又不能访问他们。我们需要访问他们从而使用他们。嗯,在这篇文章最开头的代码片段中,你看到我调用module时,我将他的返回值赋值给了一个变量,这是因为我使用非常简单的方法来同时定义和获取模块。
var myModule = App.module("myModule");
通常,如果你只是尝试获取这个模块,你会传入第一个参数,module 方法就会出去寻找到那个你需要的模块。但如果你传入了一个函数作为第二个参数,这个模块就会添加那些你的功能,然后它依然会返回你创建的或更改了的的模块。这意味着你能通过同一个方法定义你的模块并获取它。
这并不是唯一的方式进行模块的获取。当一个模块被创建时,它就会直接被附加到构建模块时所调用的Application对象上。这使得你能使用简单的对象属性获取操作来访问你的模块;但这次,这个模块必须已经在之前被定义,否则你会得到一个undefined引用。
// 同样能运作但我不推荐这样使用
var myModule = App.myModule;
虽然这样的语法更为简短,但这对于其它的开发者并没有表达相同的信息。我会更推荐使用module方法来进行模块的访问,所以非常明显地你正在访问一个模块而不是App的其它什么属性。便利与危险在于它会创建这个原来不存在的模块。如果你误拼写了这个模块的名称,你将不会有任何方式知道你并没有获取正确的模块,直到你尝试访问一个不存在的属性。(译者注:这段也翻译得感觉有点怪)
子模块
模块也能够拥有子模块。遗憾的是,Module 没有他自己的module方法,所以你不能直接地给模块添加子模块,但这并不会让我们放弃。取而代之的是,想要创建子模块,就像我们之前做的,你依然调用App的module方法;但命名这个模块时,你需要添加一个点(.)到你的父模块名称之后,然后才是子模块的名称。
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时,其它的一些事情也会发生。下面是所有会发生的事件,按顺序:
- 触发 initialize:before 事件
- 启动所有已定义的模块
- 执行所有的初始化器,依照他们添加的顺序
- 触发 initialize:after 事件
- 触发 start 事件
一个模块的表现基本也是这样。事件的数量及一些名称会不同,但大体上还是相同的过程。当一个模块启动时,它会:
- 触发 before:start 事件
- 启动所有已经定义的子模块
- 执行它的所有初始化器,依照他们添加的顺序
- 触发 initialize:after 事件
- 触发 start 事件
stop方法也非常相似。相对于添加初始化器,你需要添加终结器(finalizers)。你通过addFinalizer方法传入函数来执行进行这个操作,然后这些finalizer就会在stop调用的时候执行。不像初始化器那样,没有数据或参数会传入到每一个函数。当一个stop调用时:
- 触发 before:stop 事件
- 终止这个模块的所有子模块
- 执行所有添加的finalizers
- 触发 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:start及start事件 **
像我说的,当你调用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中的模块可以以相似地方式像其它模块定义库那样使用,但这并不是它实际上设计的意图。内建的start和stop方法就是一个迹象。这些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的所有内容,似乎还是相当有价值的。但因为版权问题我估计就不会继续翻译下去了。
转自:http://blog.waterwu.me/marionettejs-thorough-introduce-2/ 感谢原作者
转载请注明:阿尤博客 » Marionettejs 全面介绍 (2) 模块系统