/backbone.Composite

A plugin for backbone.js. Use composite pattern for backbone's views.

Primary LanguageJavaScript

backbone.Composite

这个插件致力于用组合模式更好地处理 backbone.js 中的视图关系。有下面几个特性:

  • 使应用程序的整体视图关系达到一个树形结构,尽可能地降低视图模块之间的耦合性。
  • 使用中介者模式,充分利用事件机制,使不同视图之间的可以进行通信。
  • 批量处理视图 DOM 的添加
  • 多对多事件的触发。
  • 动态事件自动绑定。

使用Composite的时机:

如果你发现自己需要在一个视图中用到另外一个视图,那么请考虑用组合视图,把那两个视图组合起来,而不是在一个视图的实现中参杂另外一个视图。

How to use

加载

要使用本插件,只要在 backbone.js 加载完成以后加载本插件就可以使用插件的功能。

<script src='jquery.js'></script>
<script src='underscore.js'></script>
<script src='backbone.js'></script>
<script src='backbone.Composite.js'></script>

基本用法

var SomeView = Backbone.Composite.extend({

        // 设置该组合视图用到的子视图
        items: {
			'container': function () {
				return new Container;
			},

			'subviews': function() {
				return [new Sub, new Sub, new Sub];
			}
		},

        // 设置视图的包含关系
		nests: {
			'container': 'subviews'
		},

        // 处理视图间的事件
		events: {
			'remove subviews': 'container.resize'
		}
		
});

var sv = new SomeView;
sv.getItem('container');
sv.setItem('specialView', new Sub);

详细用法请看 API

API

使用 backbone.Composite 要需要五个参数,四个方法。

参数

  • items
  • nests
  • events
  • exportAPI
  • exportEvent

方法

  • setItem()
  • getItem()
  • pushItem()
  • deleteItem()

items

这个对象声明了该组合视图需要用到的子视图,它是树形结构的关键,在定义一个组合视图类时传入。格式为:

    items: {
        
        itemId1: function () {
            return view;
        },
        
        itemId2: function () {
            return [view, view, view,....];
        }
    }

itemId是你为该子视图所设定的唯一标识,键值则是一个函数,返回一个 Backbone 视图对象 或者存放视图对象的数组。

    var CommentWrapper = Backbone.Composite.extend({

		items: {

			// 设置 `deleteBtn` 这个 id 对应的是一个 Button 视图实例
			'deleteBtn': function () {
				return new Button;
			},

			// `comments` 这个 id 则对应的是一组 Comment 的视图实例
			'comments': function () {
				return [new Comment, new Comment, new Comment];
			}
		}

	});
    
    // 实例化组合视图
    new CommentWrapper;
    

也许你会疑惑为什么要用函数作为键值,为什么我们不直接这样:

    var CommentWrapper = Backbone.Composite.extend({

    	items: {
		
			'deleteBtn':  new Button,
		
			'comments': [new Comment, new Comment, new Comment]
		}

	});
    
    // 实例化组合视图
    new CommentWrapper;

如果你这样做,那么所有的CommentWrapper组合视图之间会共用 'deleteBtn''comment' ,假设你实例化两次CommentWrapper:

    var cw1 = new CommentWrapper;
    var cw2 = new CommentWrapepr;

那么cw1cw2'deleteBtn''comment'将是同一个实例。 如果你需要在在组合视图之间共用子视图实例,可以用实例取代函数作为键值。但是通常情况下建议大家使用函数。


nests

这个参数处理视图的 DOM 元素之间的父子节点关系,接受数组或者返回数组的函数作为键值, 格式为:

    nests: {
        parent: [child, child, child,...],
        parent: function () {
            return [child, child, child,....];
        }
    }

其中parent可以是:

  • 本组合视图的任意非数组子视图 id
  • jQuery选择器

child可以为:

  • 本组合视图中任意视图 id,包括数组
  • 任意 Backbone 的视图实例
  • jQuery 对象
  • jQuery 选择器

当实例化一个组合视图的时候,插件会自动将数组中所有的 child 添加到 parent 的 DOM 结构中。

    var CommentWrapper = Backbone.Composite.extend({

        items: {
		
			'deleteBtn':  function () { return new Button; },
		
			'comments': function () { return [new Comment, new Comment, new Comment]},
            
            'containier': function () {return new Container;}
		},
        
        nests: {
            // 视图id     数组视图id    视图id    jQuery选择器   jQuery对象
            'container ul': ['comments', 'deleteBtn', '#sample', $('<li>item</li>')],
            
            // 选择器   视图id
            'body':     ['container']
        }

	});
    
    // 实例化组合视图
    new CommentWrapper;

上面的CommentWrapper在实例化的时候,会执行:

  1. comments这个id对应的数组子视图的三个comment$el都添加到container这个id对应的视图的DOM元素的子元素'ul'中。
  2. deleteBtn对应的视图的$el添加到container
  3. #sample选择器获得的DOM元素添加到container
  4. <li>item</li>元素插入container
  5. 然后最终将container添加到body元素中

如果parent为itemId selector,那么就相当于向该itemId对应的子视图的子DOM元素,相当于$el.find(selector),进行append操作。

假如你的子视图也是一个组合视图,你想往子视图的子视图中添加元素应该怎么做呢?插件提供强大的“点”操作, 你可以通过itemId.itemId来获取视图后继视图。继续上面的代码:

    var App = Backbone.CompositeView.extend({
        
        items: {
            // wrapper 对应的视图也是一个组合视图,也就是上面的 CommentWrapper
            'wrapper': function () {return new CommentWrapper;}
        },
        
        nests {
            // 往 wrapper 这个子视图的子视图 container 插入两个 Comment
            'wrapper.container': [new Comment, new Comment]
        }
        
    });
    
    new App;

理论上你可以通过.无限获取组合视图的子视图,itemId.itemId.itemId.itemId....。 当然,所有的点操作都应该是在你给视图所标识的id上进行。


events

视图之间都是通过事件来进行消息的传递的, 插件提供了良好的事件处理机制, 让你可以对视图进行一对多,多对一,多对多的事件绑定。

格式为:

    events: {
        'eventName itemId': [handler, hander, handler,...],
        'eventName itemId': function () {
            return [handler, hander, handler,...];
        }
    }
  1. eventName为事件的名称,
  2. itemId为触发事件子视图的id
  3. handler可以是一个字符串,也可以是一个函数。如果是字符串,则可能绑定组合视图本身或者组合视图子视图的事件, 根据是否有.操作来判断。
  4. 当实例化的时候,会把数组中所有的函数绑定到itemId对应视图的eventName事件下。

参考例子:

    var Dog = Backbone.View.extend({

		park: function () {
			// do stuff
			console.log('Dog park');
			this.trigger('park');
		},

		run: function () {
			// do stuff
			console.log('Dog run');
			this.trigger('run');
		}
	});

	var Cat = Backbone.View.extend({
		run: function () {
			// do stuff
			console.log('Cat run...');
			this.trigger('run');
		}
	});

	var People = Backbone.View.extend({
		rage: function () {
			// do stuff
			console.log('People rage');
			this.trigger('rage');
		}
	});

	var House = Backbone.Composite.extend({

		items: {
			'women': function () {
				return new People;
			},

			'cats': function () {
				return [new Cat, new Cat, new Cat];
			},

			'dogs': function () {
				return [new Dog, new Dog];
			}
		},

		events: {
			'park dogs': ['cats.run', 'women.rage'],
			'rage women': ['dogs.run', 'houseMessUp', function () {console.log('~~~funny');}]
		},

		initialize: function () {
			this.getItem('dogs')[0].park();
		},

		houseMessUp: function () {
			console.log('House Mess Up');
		}

	});

	new House;

上面的例子中,讲述了这样一个故事。一个房子里面(组合视图House)里面住着一个叫women的人(子视图People), 养了三只猫cats(子视图Cat),和两条狗dogs(子视图Dog)。在一个销魂的早上大概发生了这样事情: 其中一条狗大吠了一声,于是:

  • 把所有的猫都吓跑了,惹怒了女主人,
  • 女主人怒了,狗也全跑了。女主人追着狗跑,让房子全乱了。

于是控制台中输出:

Dog park // 一条狗吠了
Cat run... :3 // 三只猫跑了
People rage  // 主人怒了
Dog run :2 // 两条狗跑了
House Mess Up // 房子乱成一团
~~~~funny // 好好玩不是么?

我们代码中的events就通过事件达到了这种效果

    events: {
		'park dogs': ['cats.run', 'women.rage'],
		'rage women': ['dogs.run', 'houseMessUp', function () {console.log('~~~funny');}]
	},

dogs是一个存放了两个Dog视图实例的数组,cats是放了三个Cat视图实例的数组,women是一个People实例。 当dogs中任意一个视图触发了park事件,会使cats所有视图执行run方法,women执行rage方法。 当women触发了rage事件,会使dogs中所有视图调用run方法,以及执行househouseMessUp方法和一个自定义的匿名方法。

从上面的例子我们可以看出,我们的插件可以让一组或者一个子视图元素的事件监听一系列方法, 这些方法可以是

  • 自定义的
  • 子视图的
  • 组合视图的

(如果有.则是绑定子视图的方法,否则就是在该组合视图中寻找该方法 ,如上面的dogs.run绑定子视图的方法,houseMessUp绑定组合视图的方法)。

你甚至可以监听子视图的子视图的事件, 假如有比House更高层的组合视图App, 在其中可以监听Houese中的dogspark事件:

    var App = Backbone.Composite.extend({
        items: {
            'house': function () {return new House;}
        },
        events: {
    	    'park house.dogs': [function () {}...]
	    }
    });
    
    new App;

exportAPI

设置该属性可以把item的方法暴露为自己的方法:

格式为:

exportAPI: {
	'itemId': ['method1','method2',....]
}

给出一个栗子:

var Box = Backbone.View.extend({
	name: 'Hi',
	sayName: function () {
		console.log(this.name);	
	}
});

var BoxWrapper = Backbone.Composite.extend({
	items: {
		'box': function () {
			return new Box;
		}
	},
	exportAPI: {
		'box': ['sayName']
	}
});

var boxWrapper = new BoxWrapper;
boxWrapper.sayName(); // => Hi

如上面的代码,sayHi函数属于Box的方法,但是我们想把它暴露出去作为组合视图的方法,那么就可以用exportAPI


exportEvent

设置该属性可以把item的事件暴露为自己的事件

格式为:

exportEvent: {
	'itemId': ['Event1','Event2',....]
}

给出一个栗子:

var Box = Backbone.View.extend({
	name: 'Hi',
	sayName: function () {
		this.trigger('nameSay', this.name);
	}
});

var BoxWrapper = Backbone.Composite.extend({
	items: {
		'box': function () {
			return new Box;
		}
	},
	exportEvent: {
		'box': ['nameSay']
	}
});

var boxWrapper = new BoxWrapper;
boxWrapper.on('nameSay', function (name) {
	console.log('I have got ' + name + ' said');
});
boxWrapper.getItem('box').sayName(); // => I have got Hi said

如上面的代码,nameSay函数属于Box的事件,但是我们想把它暴露出去作为组合视图的事件,那么就可以用exportEvent


getItem(itemId:String)

获取组合视图的子视图,支持.操作获取更底层的视图:

var app = new App();
app.getItem('house'); // => new House
app.getItem('house.dogs'); // => [dog, dog, dog]

若视图存不存在,返回null


pushItem(itemId:String, view:Object [, options:Object])

该方法可以往组合视图的数组形式子视图中添加元素,同时,你可以通过可选参数options来设置是否需要配置函数的行为。 options的有三个可以设置的属性:nestbindlisten,都是Boolean类型。实例用法(回忆我们House的例子):

 	// 此处省略代码
	
	var House = Backbone.Composite.extend({

		items: {
			'women': function () {
				return new People;
			},

			'cats': function () {
				return [new Cat, new Cat, new Cat];
			},

			'dogs': function () {
				return [new Dog, new Dog];
			}
		},

		events: {
			'park dogs': ['cats.run', 'women.rage'],
			'rage women': ['dogs.run', 'houseMessUp', function () {console.log('~~~funny');}]
		},

		initialize: function () {
			this.getItem('dogs')[0].park();
		},

		houseMessUp: function () {
			console.log('House Mess Up');
		}

	});
	
	var house = new House;
	var dog = new Dog;
	
	house.pushItem('dogs', dog, {
		nest: true,
		bind: true,
		listen: true,
		prepend: false
	});
	
	dog.park();
  • nest:新增的视图是否也按照nests的规则进行 DOM 元素的插入。

  • bind:设置子视图的触发事件是否也按照viewsEvents的规则来执行一系列的函数。 如上面的dogpark事件的触发会导致cats.runwomen.rage函数的执行。 如果你希望新插入的小dog不会吓跑cats,不会让women发怒。那么简单地设置bindfalse即可。

  • listen:如果在viewsEvents的规则中,子视图监听了一些其他子视图的事件,那么你可以通过设置这个参数来规定 新插入的视图元素是否按照规则来继续监听其他子视图的事件。如我们的dogs是监听了womenrage事件的,一旦 women触发了rage事件,所有的dogs都会跑光光。如果你希望新插入的dog不会被women吓跑,那么简单设置listenfalse就可以。

  • prepend:默认是false,当插入 DOM 的时候是以$el.append方式插入;如果设置为true,那么就以$el.prepend方式插入DOM

假如不传入参数options,那么前三个属性默认都为true,后面一个默认为false

该方法会触发pushpush:itemId事件


setItem(itemId:String, view:Object [, options:Object])

设置子视图,不支持.操作,也就说处于安全考虑,不可以让你设置后继子视图。

var app = new App();
app.setItem('anotherHouse', new House, {
	nest: true,
	bind: true,
	listen: true,
	remove: true
});

该方法会触发getget:itemId事件,如果是添加本身不存在的视图同时会触发add事件。 参数 nestbindlistenpushItem API含义相同,remove 参数用来设置是否把原来的item对应的DOM元素也一并删除。 默认所有参数都为true


deleteItem(itemId:String [,options:Object])

让你可以从实例中删除子视图,相应的DOM元素并不会删除。需要你手动删除。

var app = new App();
app.deleteItem('house', {
	remove: true
});

该方法会触发deletedelete:ItemId事件 接受一个参数 remove, 用来设置在删除item的时候是否把item对应的DOM元素也从DOM树中删除。

Licence

MIT