高级发布机制
目前你应该对发布和订阅交互模式有一个不错的掌握了。因此,我们废话少说,来看几个更高级的情景。
多次发布一个集合
在我们第一个关于发布的附录中,我们看到了一些更普遍的发布和订阅模式,同时我们学习了 _publishCursor
函数,如何让它们非常容易地实现在我们的站点上。
首先,让我们回忆 _publishCursor
到底为我们做了什么:它将整理所有的文档以匹配一个给定的游标(cursor),并将它们推送至同名的客户端集合中。注意这与 publication 的名字是不关联的。
这意味着我们可以用不止一个 publicaton 去连接任何集合的客户端与服务端版本。
我们已经在分页章节用过这个模式,当我们在当前显示的帖子之外,再发布一个所有帖子的分页后的子集。
另一个相似的用例是发布一大组文档的预览,和单个文档的全部信息:
Meteor.publish('allPosts', function() {
return Posts.find({}, {fields: {title: true, author: true}});
});
Meteor.publish('postDetail', function(postId) {
return Posts.find(postId);
});
现在客户端订阅这两个发布,这 'posts'
集合来自于两个源渠道:来自第一个订阅的标题和作者姓名列表,和来自第二个订阅的单个帖子全部信息。
你也许意识到了 postDetail
发布的帖子也被 allPosts
发布了(尽管只有它的部分属性)。但是,Meteor 会合并字段及确认没有重复的帖子,来处理数据重叠的问题。
这是很棒的,因为现在当我们呈现帖子摘要列表时,我们正在处理的数据对象正好拥有我们需要显示的足够数据。但是,当我们呈现单个帖子时,我们有一切需要展示的数据。当然,在这种情况下,我们需要让客户端不要去期待所有帖子的所有字段都能都显示出来————这是一个常见的问题!
注意你并没有改变文档属性的任何限制。你可以很好地在这两个发布中发布同样的属性,但是先后排序不同。
Meteor.publish('newPosts', function(limit) {
return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});
Meteor.publish('bestPosts', function(limit) {
return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
多次订阅一个发布
我们已经看了如何多次发布同一个集合。事实证明你可以通过另一个模式来完成非常相近的结果:建立一个单一发布,却多次订阅它。
在 Microscope 中,我们多次重复订阅 posts
发布,但 Iron Router 为我们设置并拆开每次的订阅。然而,没有理由我们不能同时进行多次订阅。
举个例子,我们想要将最新的和最好的帖子同时载入内存:
我们设定一个单一发布:
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
并且我们多次订阅这个发布。事实上或多或少我们在 Microscope 里这样做了:
Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
接下来到底发生什么了?每个浏览器开启了两个不同的订阅,每个订阅连接到同个服务端的发布。
每个订阅提供了不同的发布参数,但从根本上,每次一个(不同)文档子集从 posts
集合提取出来,并通过连接机制发送到客户端集合。
你甚至可以用同样的参数订阅两次相同的发布。这个对很多处场景来说很难说有用,但这种弹性机制总有一天会有用的。
单一订阅中的多个集合
不像传统关系型数据库像 MySQL 使用 joins,NoSQL 数据库类似 Mongo 都是关于去规范化和嵌入。让我们看看它们是怎样在 Meteor 环境下工作的。
让我们看一个具体的例子。我们已经对我们的帖子添加了评论,到目前为止,我们一直很愉快地只发布用户看的单个帖子的评论。
但是,假设我们希望在首页中显示全部帖子的回复(记着这些帖子会在分页时被改变)。这个用例展示了一个很好的理由把评论嵌入帖子中,事实上这是促使我们来非规范化评论数量。
当然我们可以总是嵌入评论到帖子中,并完全摒除 Comments
集合。但如同我们前面在去规范化章节看到的,我们将在分离的集合的操作中也会失去一些额外的好处。
但是事实证明有一个涉及订阅的技巧,在保持分离集合的同时再嵌入我们的评论。
让我们假定除了首页帖子列表之外,我们希望再订阅每个帖子的两个最新评论。
使用独立的评论发布会很难完成这个要求,尤其在帖子列表受到某些限制时(比如说,最近的10个)。我们必须写一个发布,看起来像下面的代码:
Meteor.publish('topComments', function(topPostIds) {
return Comments.find({postId: topPostIds});
});
从性能角度来看这是个问题,因为这个发布将需要每次在 topPostIds
改变时消除及重新建立。
有一个途径来解决这个问题。我们可以应用这个事实:就是我们不仅可以在每个集合上拥有多次发布,而且我们也可以在每个发布上拥有多个集合。
Meteor.publish('topPosts', function(limit) {
var sub = this, commentHandles = [], postHandle = null;
// send over the top two comments attached to a single post
function publishPostComments(postId) {
var commentsCursor = Comments.find({postId: postId}, {limit: 2});
commentHandles[postId] =
Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
}
postHandle = Posts.find({}, {limit: limit}).observeChanges({
added: function(id, post) {
publishPostComments(id);
sub.added('posts', id, post);
},
changed: function(id, fields) {
sub.changed('posts', id, fields);
},
removed: function(id) {
// stop observing changes on the post's comments
commentHandles[id] && commentHandles[id].stop();
// delete the post
sub.removed('posts', id);
}
});
sub.ready();
// make sure we clean everything up (note `_publishCursor`
// does this for us with the comment observers)
sub.onStop(function() { postHandle.stop(); });
});
注意我们在这个发布中没有返回任何东西,因为我们自己手动给 sub
发送信息(通过 .added()
等方式)。所以我们不必通过返回一个游标来请求 _publishCursor
给我们来做这个动作。
现在,每次我们发布一个帖子时,我们也自动发布其2个最新评论。而且所有都在一个订阅调用中!
虽然 Meteor 还未直接实现这个方法,但是你也可以参考在 Atomsphere 里的 publish-with-relations
包,它的目标就是让这个模式更容易使用。
连接不同的集合
这样的订阅弹性机制还能给我们更多新知识么?当然,如果我们不使用 _publishCursor
,我们不必跟着此项约束,就是在服务端的源集合需要与客户端的目标集合有同样的名称。
为什么我们想这么做的一个原因就是单表继承。
假设我们需要从我们的帖子中引用多种类型的对象,每一个对象存储在相同字段中但又显然是不同内容。例如,我们建立一个类似 Tumblr 的博客引擎,每个帖子具有常见的 ID、时间戳,以及标题;但是额外也有如图像、视频、链接,或者只是文字。
我们可以将这些对象存储在一个单独的 'resources'
集合中,使用 type
属性来标记他们是什么类型的对象(video
、image
、link
等等)。
同时,虽然我们有一个服务端的单一的 Resources
集合,我们也能够将单一集合转换到多个的 Videos
、`Images',等等集合中。
客户端的集合如下的代码:
Meteor.publish('videos', function() {
var sub = this;
var videosCursor = Resources.find({type: 'video'});
Mongo.Collection._publishCursor(videosCursor, sub, 'videos');
// _publishCursor doesn't call this for us in case we do this more than once.
sub.ready();
});
我们告诉 _publishCursor
(就像返回)游标会做的一样发布我们的视频,但不是将 resources
集合发布到客户端,而是我们从 resources
发布到 videos
。
另一个类似的主意是:发布到客户端的集合,却根本没有服务端集合!举例,你也许从一个第三方服务中抓取数据,并发布它们到一个客户端集合。
由于发布 API 的灵活性,可能性是无限的。