高级发布机制

优质
小牛编辑
134浏览
2023-12-01

目前你应该对发布和订阅交互模式有一个不错的掌握了。因此,我们废话少说,来看几个更高级的情景。

多次发布一个集合

在我们第一个关于发布的附录中,我们看到了一些更普遍的发布和订阅模式,同时我们学习了 _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 属性来标记他们是什么类型的对象(videoimagelink 等等)。

同时,虽然我们有一个服务端的单一的 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 的灵活性,可能性是无限的。