高级发布机制

附录 13.5

翻译进度

本章内容:

  • 学习更多处理发布机制的高级方法。
  • 了解发布/订阅模型可以得到怎样的弹性机制。
  • 目前你应该对发布和订阅交互模式有一个不错的掌握了。因此,我们废话少说,来看几个更高级的情景。

    多次发布一个集合

    我们第一个关于发布的附录中,我们看到了一些更普遍的发布和订阅模式,同时我们学习了 _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});
    });
    
    server/publications.js

    多次订阅一个发布

    我们已经看了如何多次发布同一个集合。事实证明你可以通过另一个模式来完成非常相近的结果:建立一个单一发布,却多次订阅它。

    在 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 的灵活性,可能性是无限的。