理解Meteor的发布/订阅功能

84

我已经设置好了一个简单的应用程序,显示了一个 Projects 列表。我已经移除了 autopublish 包,这样我就不会将所有内容发送到客户端。

 <template name="projectsIndex">    
   {{#each projects}}      
     {{name}}
   {{/each}}
 </template>

当启用autopublish时,将显示所有项目:

if Meteor.isClient
  Template.projectsIndex.projects = Projects.find()

如果将其移除,则需要进行以下附加操作:

 if Meteor.isServer
   Meteor.publish "projects", ->
     Projects.find()
 if Meteor.isClient
   Meteor.subscribe "projects"
   Template.projectsIndex.projects = Projects.find()

那么,可以说客户端的find()方法只搜索从服务器端发布的记录吗?这让我感到困惑,因为我觉得我只需要调用一次find()

4个回答

287

集合、发布和订阅是Meteor中一个棘手的领域,文档可以更详细地讨论这些问题,以避免频繁 混淆,有时由于混淆的术语而被放大。

这里是Sacha GreifDiscoverMeteor的合著者)在一张幻灯片中解释发布和订阅:

subscriptions

要正确理解为什么需要多次调用find(),您需要了解Meteor中集合、发布和订阅的工作方式:

  1. You define collections in MongoDB. No Meteor involved yet. These collections contain database records (also called "documents" by both Mongo and Meteor, but a "document" is more general than a database record; for instance, an update specification or a query selector are documents too - JavaScript objects containing field: value pairs).

  2. Then you define collections on the Meteor server with

    MyCollection = new Mongo.Collection('collection-name-in-mongo')
    

    These collections contain all the data from the MongoDB collections, and you can run MyCollection.find({...}) on them, which will return a cursor (a set of records, with methods to iterate through them and return them).

  3. This cursor is (most of the time) used to publish (send) a set of records (called a "record set"). You can optionally publish only some fields from those records. It is record sets (not collections) that clients subscribe to. Publishing is done by a publish function, which is called every time a new client subscribes, and which can take parameters to manage which records to return (e.g. a user id, to return only that user's documents).

  4. On the client, you have Minimongo collections that partially mirror some of the records from the server. "Partially" because they may contain only some of the fields, and "some of the records" because you usually want to send to the client only the records it needs, to speed up page load, and only those it needs and has permission to access.

    Minimongo is essentially an in-memory, non-persistent implementation of Mongo in pure JavaScript. It serves as a local cache that stores just the subset of the database that this client is working with. Queries on the client (find) are served directly out of this cache, without talking to the server.

    These Minimongo collections are initially empty. They are filled by

    Meteor.subscribe('record-set-name')
    

    calls. Note that the parameter to subscribe isn't a collection name; it's the name of a record set that the server used in the publish call. The subscribe() call subscribes the client to a record set - a subset of records from the server collection (e.g. most recent 100 blog posts), with all or a subset of the fields in each record (e.g. only title and date). How does Minimongo know into which collection to place the incoming records? The name of the collection will be the collection argument used in the publish handler's added, changed, and removed callbacks, or if those are missing (which is the case most of the time), it will be the name of the MongoDB collection on the server.

修改记录

这是Meteor非常方便的地方:当您在客户端修改Minimongo集合中的记录(文档)时,Meteor会立即更新所有依赖它的模板,并将更改发送回服务器,服务器将把更改存储在MongoDB中,并将其发送到已订阅包含该文档的记录集的适当客户端。这被称为延迟补偿,是Meteor的七个核心原则之一。

多次订阅

您可以有一堆订阅来拉取不同的记录,但如果它们来自服务器上的同一集合并基于它们的_id,它们最终都会进入客户端的同一个集合。这在Meteor文档中没有明确解释,但是暗示了这一点。

当您订阅记录集时,它会告诉服务器向客户端发送记录。客户端将这些记录存储在本地的Minimongo集合中,名称与发布处理程序中使用的collection参数相同,包括addedchangedremoved回调。Meteor将排队接收的属性,直到您在客户端上声明具有匹配集合名称的Mongo.Collection。
未被解释的是,当您不显式使用addedchangedremoved或根本不使用发布处理程序时会发生什么-这是大多数情况。在这种最常见的情况下,集合参数(毫不奇怪)取自您在第1步中在服务器上声明的MongoDB集合的名称。但是这意味着您可以具有不同名称的不同发布和订阅,并且所有记录都将在客户端的同一集合中结束。Meteor会在文档的顶级字段层面执行集合并,以便订阅可以重叠-将不同顶级字段发送到客户端的发布函数并排工作,在客户端上,集合中的文档将是两个字段集的并集

示例:多个订阅填充客户端上的同一集合

您有一个BlogPosts集合,它在服务器和客户端上以相同的方式声明,即使它执行不同的操作:

BlogPosts = new Mongo.Collection('posts');

在客户端,BlogPosts 可以从以下获取记录:
  1. a subscription to the most recent 10 blog posts

    // server
    Meteor.publish('posts-recent', function publishFunction() {
      return BlogPosts.find({}, {sort: {date: -1}, limit: 10});
    }
    // client
    Meteor.subscribe('posts-recent');
    
  2. a subscription to the current user's posts

    // server
    Meteor.publish('posts-current-user', function publishFunction() {
      return BlogPosts.find({author: this.userId}, {sort: {date: -1}, limit: 10});
      // this.userId is provided by Meteor - http://docs.meteor.com/#publish_userId
    }
    Meteor.publish('posts-by-user', function publishFunction(who) {
      return BlogPosts.find({authorId: who._id}, {sort: {date: -1}, limit: 10});
    }
    
    // client
    Meteor.subscribe('posts-current-user');
    Meteor.subscribe('posts-by-user', someUser);
    
  3. a subscription to the most popular posts

  4. etc.
所有这些文档来自于 MongoDB 中的“posts”集合,通过服务器上的“BlogPosts”集合,并最终进入客户端中的“BlogPosts”集合。
现在我们可以理解为什么需要调用find()超过一次 - 第二次是在客户端进行,因为所有订阅的文档最终都会进入同一个集合,你只需要获取你关心的文档。例如,要在客户端获取最新的帖子,只需复制服务器上的查询即可:
var recentPosts = BlogPosts.find({}, {sort: {date: -1}, limit: 10});

这将返回客户端到目前为止已收到的所有文档/记录的光标,包括置顶帖和用户的帖子。(感谢 Geoffrey)。

10
很好。也许值得一提的是,在订阅了两个发布后,如果在客户端执行BlogPosts.find({})会发生什么——它将返回当前在客户端上的所有文档/记录的光标,包括置顶帖子和用户帖子。我在SO上看到过其他一些问题,问者对此感到困惑。 - Geoffrey Booth
3
太好了,谢谢。此外,Meteor.users()集合在客户端上自动发布,这可能会有些令人困惑。能否在上面的回答中添加一点内容来说明users()集合? - mirageglobe
3
即使超出最初的要求,我认为@DVG应该将这篇优秀的写作标记为被接受的答案。感谢丹。 - physiocoder
4
是否可以仅调用您订阅的记录集?也就是说,是否可以直接在JavaScript中获取记录集,而不是在本地查询Minimongo数据库? - Jimmy Knoot
1
@DanDascalescu 是的,那正是我想说的。我明白了,真遗憾,无论如何还是谢谢 :) - Jimmy Knoot
显示剩余13条评论

27

是的,客户端的find()只返回在Minimongo中的文档。来自文档:

在客户端,创建了一个Minimongo实例。Minimongo本质上是Mongo的一个内存中、非持久化的纯JavaScript实现。它作为一个本地缓存,仅存储此客户端正在处理的数据库子集。在客户端上查询(find)直接从此缓存中提供,无需与服务器通信。

正如你所说,publish()指定了客户端将拥有哪些文档。


1

基本的规则是,在客户端和服务器端,publishsubscribed 变量名应该相同。

Mongo DB 中的集合名称和客户端的名称应该相同。

假设我正在使用发布和订阅来处理名为 employees 的集合,则代码如下:


服务器端

这里使用var关键字是可选的(使用此关键字将使集合局部化到此文件)。

CollectionNameOnServerSide = new Mongo.Collection('employees');   

Meteor.publish('employeesPubSub', function() { 
    return CollectionNameOnServerSide.find({});     
});

客户端.js文件
CollectionNameOnClientSide = new Mongo.Collection('employees');
var employeesData = Meteor.subscribe('employeesPubSub');

Template.templateName.helpers({
  'subcribedDataNotAvailable' : function(){
        return !employeesData.ready();
    },
   'employeeNumbers' : () =>{
       CollectionNameOnClientSide.find({'empId':1});
  }
});

客户端 .html 文件

在这里,我们可以使用 subcribedDataNotAvailable 辅助方法来判断客户端是否准备好数据,如果数据已准备好,则使用 employeeNumbers 辅助方法打印员工编号。

<TEMPLATE name="templateName">
{{#if subcribedDataNotAvailable}}
   <h1> data loading ... </h1>
 {{else}}
  {{#each employeeNumbers }}
     {{this}}
  {{/each}}
 {{/if}}
<TEMPLATE>

0
// on the server
Meteor.publish('posts', function() {

    return Posts.find();

});

// on the client
Meteor.subscribe('posts');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接