出版物和数据加载

如何以及在哪里使用出版物和订阅在您的 Meteor 应用中加载数据。

阅读本指南后,您将了解

  1. Meteor 平台中的出版物和订阅是什么。
  2. 如何在服务器上定义出版物。
  3. 在客户端和哪些模板中订阅。
  4. 管理订阅的有用模式。
  5. 如何以反应式方式发布相关数据。
  6. 如何在面对反应式更改时确保您的出版物是安全的。
  7. 如何使用低级发布 API 发布任何内容。
  8. 订阅出版物时会发生什么。
  9. 如何将第三方 REST 端点转换为出版物。
  10. 如何将应用中的出版物转换为 REST 端点。

出版物和订阅

在传统的基于 HTTP 的 Web 应用中,客户端和服务器以“请求-响应”方式进行通信。通常客户端向服务器发出 RESTful HTTP 请求并接收 HTML 或 JSON 数据作为响应,并且服务器无法在后端发生更改时“推送”数据到客户端。

Meteor 从一开始就构建在分布式数据协议 (DDP) 上,以允许双向数据传输。构建 Meteor 应用不需要您设置 REST 端点来序列化和发送数据。相反,您创建可以将数据从服务器推送到客户端的出版物端点。

在 Meteor 中,**出版物**是服务器上一个命名的 API,用于构建要发送到客户端的数据集。客户端启动一个**订阅**,该订阅连接到出版物并接收该数据。该数据包括在初始化订阅时发送的第一批数据,以及发布的数据发生更改时的增量更新。

因此,订阅可以被认为是一组随时间变化的数据。通常,其结果是订阅“桥接”了服务器端 MongoDB 集合客户端 Minimongo 缓存。您可以将订阅视为一个管道,它将“真实”集合的一部分与客户端的版本连接起来,并不断使其与服务器上的最新信息保持同步。

定义出版物

出版物应在仅限服务器的文件中定义。例如,在 Todos 示例应用中,我们希望将公共列表集发布给所有用户

Meteor.publish('lists.public', function() {
  return Lists.find({
    userId: {$exists: false}
  }, {
    fields: Lists.publicFields
  });
});

关于此代码块,需要了解一些事项。首先,我们使用唯一的字符串lists.public命名了出版物,这将是我们从客户端访问它的方式。其次,我们从发布函数返回一个 Mongo 游标。请注意,游标已过滤,仅返回集合中的某些字段,如安全文章中所述。

这意味着出版物将确保与该查询匹配的数据集可用于订阅它的任何客户端。在本例中,所有没有userId设置的列表。因此,客户端上名为Lists的集合将具有服务器上名为Lists的集合中可用的所有公共列表,同时该订阅处于打开状态。在这个 Todos 应用中的特定示例中,订阅在应用启动时初始化并且从未停止,但后面的部分将讨论订阅生命周期

每个出版物都采用两种类型的参数

  1. this上下文,其中包含有关当前 DDP 连接的信息。例如,您可以使用this.userId访问当前用户的_id
  2. 发布的参数,这些参数可以在调用Meteor.subscribe时传入。

注意:由于我们需要访问this上的上下文,因此我们需要对发布使用function() {}形式,而不是 ES2015 的() => {}。您可以使用eslint-disable prefer-arrow-callback为发布文件禁用箭头函数 lint 规则。未来版本的发布 API 将与 ES2015 更好地配合使用。

在此加载私有列表的出版物中,我们需要使用this.userId仅获取属于特定用户的待办事项列表。

Meteor.publish('lists.private', function() {
  if (!this.userId) {
    return this.ready();
  }

  return Lists.find({
    userId: this.userId
  }, {
    fields: Lists.publicFields
  });
});

由于 DDP 和 Meteor 账户系统提供的保证,上述出版物可以确信它只会将私有列表发布给属于它们的用户。请注意,如果用户注销(或再次登录),出版物将重新运行,这意味着已发布的私有列表集将随着活动用户的更改而更改。

对于注销的用户,我们显式调用this.ready(),这表示订阅已发送了我们最初要发送的所有数据(在本例中为无)。重要的是要知道,如果您不从出版物返回游标或调用this.ready(),则用户的订阅将永远不会准备好,并且他们可能会永远看到加载状态。

这是一个带有名称参数的出版物的示例。请注意,检查通过网络传入的参数类型非常重要。

Meteor.publish('todos.inList', function(listId) {
  // We need to check the `listId` is the type we expect
  new SimpleSchema({
    listId: {type: String}
  }).validate({ listId });

  // ...
});

当我们在客户端订阅此出版物时,我们可以通过Meteor.subscribe()调用提供此参数

Meteor.subscribe('todos.inList', list._id);

组织出版物

将出版物放在其目标功能旁边是有意义的。例如,有时出版物提供非常具体的数据,这些数据实际上仅对为其开发的视图有用。在这种情况下,将出版物与视图代码放在同一个模块或目录中非常有意义。

但是,出版物通常更通用。例如,在 Todos 示例应用中,我们创建了一个todos.inList出版物,它发布列表中的所有待办事项。尽管在应用中我们只在一个地方使用它(在Lists_show模板中),但在较大的应用中,我们很有可能需要在其他地方访问列表的所有待办事项。因此,将出版物放在todos包中是一种明智的方法。

订阅数据

要使用出版物,您需要在客户端创建一个订阅。为此,您需要使用出版物的名称调用Meteor.subscribe()。当您执行此操作时,它会打开到该出版物的订阅,并且服务器开始通过网络发送数据,以确保您的客户端集合包含出版物指定的数据的最新副本。

Meteor.subscribe()还返回一个“订阅句柄”,它具有一个名为.ready()的属性。这是一个反应式函数,当出版物标记为就绪时返回true(您显式调用this.ready()或发送返回游标的初始内容)。

const handle = Meteor.subscribe('lists.public');

停止订阅

订阅句柄还有另一个重要的属性,即.stop()方法。当您订阅时,务必确保在完成订阅后始终在订阅上调用.stop()。这确保了订阅发送的文档将从您的本地 Minimongo 缓存中清除,并且服务器停止执行为服务您的订阅所需的工作。如果您忘记调用 stop,则会在客户端和服务器上消耗不必要的资源。

但是,如果您在反应式上下文中(例如autorun或 React 中的getMeteorData)或通过 Blaze 组件中的this.subscribe()有条件地调用Meteor.subscribe(),则 Meteor 的反应式系统将在适当的时间自动为您调用this.stop()

在 UI 组件中订阅

最好将订阅放置在尽可能靠近需要订阅数据的位置。这减少了“远距离操作”,并使理解数据在应用中的流动变得更容易。如果订阅和获取是分开的,则并不总是清楚订阅的更改(例如更改参数)将如何以及为什么影响游标的内容。

在实践中,这意味着您应该在组件中放置订阅调用。在 Blaze 中,最好在onCreated()回调中执行此操作

Template.Lists_show_page.onCreated(function() {
  this.getListId = () => FlowRouter.getParam('_id');

  this.autorun(() => {
    this.subscribe('todos.inList', this.getListId());
  });
});

在此代码片段中,我们可以看到在 Blaze 模板中订阅的两种重要技术

  1. 调用this.subscribe()(而不是Meteor.subscribe),它将一个特殊的subscriptionsReady()函数附加到模板实例,当在此模板内进行的所有订阅都准备好时,该函数为真。

  2. 调用this.autorun设置了一个反应式上下文,每当反应式函数this.getListId()更改时,它将重新初始化订阅。

Blaze 文章中阅读有关 Blaze 订阅的更多信息,以及在UI 文章中阅读有关在 UI 组件内跟踪加载状态的信息。

获取数据

订阅数据会将其放入您的客户端集合中。要使用用户界面中的数据,您需要查询您的客户端集合。在执行此操作时,需要遵循以下几个重要规则。

始终使用特定的查询来获取数据

如果您正在发布数据的一个子集,则可能会尝试查询集合中所有可用的数据(即Lists.find()),以在客户端获取该子集,而无需重新指定最初用于发布该数据的 Mongo 选择器。

但是,如果您这样做,那么如果另一个订阅将数据推送到同一个集合,则您将面临问题,因为Lists.find()返回的数据可能不再是您期望的了。在积极开发的应用中,通常很难预测将来可能发生什么变化,这可能是难以理解的错误的根源。

此外,在订阅之间切换时,有一个短暂的时期,两个订阅都已加载(请参阅下面的更改参数时的出版物行为),因此,在执行分页等操作时,这极有可能发生。

在您订阅数据附近获取数据

我们这样做是为了与我们在组件中订阅的原因相同——避免远距离操作并使理解数据来自何处变得更容易。一种常见的模式是在父模板中获取数据,然后将其传递给“纯”子组件,正如我们将在UI 文章中看到的那样。

请注意,第二个规则有一些例外情况。一个常见的例外是Meteor.user()——虽然严格来说它是已订阅的(通常是自动的),但将其作为参数通过组件层次结构传递给每个组件通常过于复杂。但是请记住,最好不要在太多地方使用它,因为它会使组件难以测试。

全局订阅

在某些情况下,您可能很想不在组件内部进行订阅,例如访问您知道始终需要的数据。例如,订阅用户对象上的额外字段(请参阅账户文章),您需要在应用程序的每个屏幕上使用这些字段。

但是,通常最好使用布局组件(您将所有组件都包装在其中)来订阅此订阅。在处理此类事情时保持一致性更好,并且如果将来决定某个屏幕不需要这些数据,这将使系统更灵活。

数据加载模式

在 Meteor 应用程序中,客户端有一些常见的数据加载和管理模式值得了解。我们将在UI/UX 文章中详细介绍其中的一些模式。

订阅就绪状态

必须了解的是,订阅不会立即提供其数据。在客户端订阅数据与数据从服务器上的发布到达之间存在延迟。您还应该注意,对于生产环境中的用户,此延迟可能比您在开发环境中本地看到的延迟长得多!

尽管 Tracker 系统意味着您在构建应用程序时通常不需要过多考虑这一点,但通常,如果您希望获得良好的用户体验,则需要知道数据何时准备就绪。

要找出这一点,Meteor.subscribe() 和(在 Blaze 组件中为 this.subscribe())返回一个“订阅句柄”,其中包含一个名为 .ready() 的响应式数据源。

const handle = Meteor.subscribe('lists.public');
Tracker.autorun(() => {
  const isReady = handle.ready();
  console.log(`Handle is ${isReady ? 'ready' : 'not ready'}`);
});

如果您订阅了多个发布,则可以创建一个句柄数组并使用every来确定所有句柄是否都已准备就绪。

const handles = [
  Meteor.subscribe('lists.public'),
  Meteor.subscribe('todos.inList'),
];

Tracker.autorun(() => {
  const areReady = handles.every(handle => handle.ready());
  console.log(`Handles are ${areReady ? 'ready' : 'not ready'}`);
});

我们可以使用此信息更巧妙地控制何时尝试向用户显示数据以及何时显示加载屏幕。

响应式更改订阅参数

我们已经看到一个使用 autorun 的示例,当订阅的(响应式)参数发生变化时重新订阅。值得更详细地研究一下在这种情况下会发生什么。

Template.Lists_show_page.onCreated(function() {
  this.getListId = () => FlowRouter.getParam('_id');

  this.autorun(() => {
    this.subscribe('todos.inList', this.getListId());
  });
});

在我们的示例中,autorun 将在 this.getListId() 发生变化时重新运行(最终是因为 FlowRouter.getParam('_id') 发生变化),尽管其他常见的响应式数据源包括:

  1. 模板数据上下文(您可以使用 Template.currentData() 响应式地访问)。
  2. 当前用户状态(Meteor.user()Meteor.loggingIn())。
  3. 其他特定应用程序客户端数据存储的内容。

从技术上讲,当这些响应式数据源之一发生变化时,会发生以下情况:

  1. 响应式数据源使 autorun 计算失效(将其标记为在下一次 Tracker 刷新周期中重新运行)。
  2. 订阅检测到这一点,并且考虑到在下一次计算运行中任何事情都可能发生,因此将自身标记为要销毁。
  3. 计算重新运行,并使用相同的或不同的参数重新调用 .subscribe()
  4. 如果订阅使用相同的参数运行,则“新”订阅会发现周围存在旧的“已标记为销毁”的订阅,并且已准备好相同的数据,并重新使用该订阅。
  5. 如果订阅使用不同的参数运行,则会创建一个新的订阅,该订阅连接到服务器上的发布。
  6. 在刷新周期结束时(即计算完成重新运行后),旧的订阅会检查它是否被重新使用,如果没有,则向服务器发送消息以告诉服务器将其关闭。

上面第 4 步是一个重要的细节——系统巧妙地知道,如果 autorun 重新运行并使用完全相同的参数订阅,则无需重新订阅。即使新的订阅是在模板层次结构中的其他地方设置的,这也适用。例如,如果用户在两个都订阅完全相同订阅的页面之间导航,则相同的机制将启动,并且不会发生不必要的订阅。

参数更改时的发布行为

还值得了解一下当新的订阅启动而旧的订阅停止时,服务器上会发生什么。

服务器明确地等待,直到所有数据都发送完毕(新订阅准备就绪)用于新订阅,然后再从旧订阅中删除数据。这样做的目的是避免闪烁——如果需要,您可以继续显示旧订阅的数据,直到新数据准备就绪,然后立即切换到新订阅的完整数据集。

这意味着,通常在更改订阅时,会有一段时间您处于订阅过多的状态,并且客户端上的数据比您严格要求的多。这是您始终应获取与已订阅数据相同的数据(不要“过度获取”)的一个非常重要的原因。

分页订阅

一种非常常见的数据访问模式是分页。它指的是一次获取一个“页面”的有序数据列表的做法——通常是若干项,例如 20 项。

两种常用的分页样式是“逐页”样式——您一次只显示一个结果页面,从某个偏移量开始(用户可以控制),以及“无限滚动”样式,您随着用户在列表中移动而显示越来越多的页面项目(这是典型的“Feed”样式用户界面)。

在本节中,我们将考虑第二种无限滚动样式分页的发布/订阅技术。由于难以在客户端计算偏移量,因此逐页技术在 Meteor 中处理起来有点棘手。如果您需要这样做,您可以遵循此处使用的许多相同技术,并使用percolate:find-from-publication包来跟踪哪些记录来自您的发布。

在无限滚动发布中,我们需要向发布添加一个新的参数来控制要加载多少项。假设我们想要对 Todos 示例应用程序中的待办事项进行分页:

const MAX_TODOS = 1000;

Meteor.publish('todos.inList', function(listId, limit) {
  new SimpleSchema({
    listId: { type: String },
    limit: { type: Number }
  }).validate({ listId, limit });

  const options = {
    sort: {createdAt: -1},
    limit: Math.min(limit, MAX_TODOS)
  };

  // ...
});

重要的是,我们在查询中设置了 sort 参数(以确保在请求更多页面时列表项的顺序可重复),并且我们对用户可以请求的项目数量设置了绝对最大值(至少在列表可以无限增长的情况下)。

然后在客户端,我们将设置某种响应式状态变量来控制要请求多少项:

Template.Lists_show_page.onCreated(function() {
  this.getListId = () => FlowRouter.getParam('_id');

  this.autorun(() => {
    this.subscribe('todos.inList',
      this.getListId(), this.state.get('requestedTodos'));
  });
});

当用户点击“加载更多”(或者可能是在他们滚动到页面底部时),我们将增加 requestedTodos 变量。

分页数据时非常有用的一条信息是您可能看到的项目总数tmeasday:publish-counts包可用于发布此信息。我们可以添加一个 Lists.todoCount 发布,如下所示:

Meteor.publish('Lists.todoCount', function({ listId }) {
  new SimpleSchema({
    listId: {type: String}
  }).validate({ listId });

  Counts.publish(this, `Lists.todoCount.${listId}`, Todos.find({listId}));
});

然后在客户端,订阅该发布后,我们可以使用以下方法访问计数:

Counts.get(`Lists.todoCount.${listId}`)

使用响应式存储的客户端数据

在 Meteor 中,持久性或共享数据通过发布在网络上传输。但是,某些类型的数据不需要持久化或在用户之间共享。例如,当前用户的“登录状态”或他们当前正在查看的路由。

虽然客户端状态通常最好作为单个模板的状态包含(并在必要时作为参数向下传递到模板层次结构),但有时您需要在模板层次结构的不相关部分之间共享的“全局”状态。

通常,此类状态存储在一个全局单例对象中,我们可以将其称为存储。单例是一种数据结构,逻辑上只存在一个副本。上面提到的当前用户和路由是此类全局单例的典型示例。

存储类型

在 Meteor 中,最好使存储成为响应式数据源,因为这样它们最自然地与生态系统的其余部分结合在一起。有一些不同的包可用于存储。

如果存储是一维的,则可以使用 ReactiveVar 来存储它(由reactive-var包提供)。ReactiveVar 具有两个属性,get()set()

DocumentHidden = new ReactiveVar(document.hidden);
$(window).on('visibilitychange', (event) => {
  DocumentHidden.set(document.hidden);
});

如果存储是多维的,则可能需要使用 ReactiveDict(来自reactive-dict包)。

const $window = $(window);
function getDimensions() {
  return {
    width: $window.width(),
    height: $window.height()
  };
};

WindowSize = new ReactiveDict();
WindowSize.set(getDimensions());
$window.on('resize', () => {
  WindowSize.set(getDimensions());
});

ReactiveDict 的优点是您可以单独访问每个属性(WindowSize.get('width')),并且字典将对字段进行差异化并单独跟踪其更改(例如,您的模板将减少重新渲染次数)。

如果您需要查询存储或存储许多相关项,则可能最好使用本地集合(请参阅集合文章)。

访问存储

您应该以与访问模板中的其他响应式数据相同的方式访问存储——这意味着集中您的存储访问,就像您集中订阅和数据获取一样。对于 Blaze 模板,这要么在帮助器中,要么在 onCreated() 回调内的 this.autorun() 中。

这样,您就可以获得存储的全部响应式功能。

更新存储

如果您需要根据用户操作更新存储,则应从事件处理程序中更新存储,就像调用方法一样。

如果您需要在更新中执行复杂的逻辑(例如,不仅仅是调用 .set() 等),则最好在存储上定义一个修改器。由于存储是单例,因此您可以直接将函数附加到对象上:

WindowSize.simulateMobile = (device) => {
  if (device === 'iphone6s') {
    this.set({width: 750, height: 1334});
  }
}

高级发布

有时,从发布函数返回查询的机制无法满足您的需求。在这些情况下,您可以使用一些更强大的发布模式。

发布关联数据

在给定页面上,通常需要来自多个集合的相关数据集。例如,在 Todos 应用程序中,当我们渲染待办事项列表时,我们希望获取列表本身以及属于该列表的待办事项集。

您可以通过从发布函数返回多个游标来实现这一点:

Meteor.publish('todos.inList', function(listId) {
  new SimpleSchema({
    listId: {type: String}
  }).validate({ listId });

  const list = Lists.findOne(listId);

  if (list && (!list.userId || list.userId === this.userId)) {
    return [
      Lists.find(listId),
      Todos.find({listId})
    ];
  } else {
    // The list doesn't exist, or the user isn't allowed to see it.
    // In either case, make it appear like there is no list.
    return this.ready();
  }
});

但是,此示例的效果可能与您预期不符。原因是在服务器上,响应式的工作方式与在客户端上的不同。在客户端上,如果响应式函数中的任何内容发生变化,整个函数都会重新运行,结果相当直观。

但是,在服务器上,响应式仅限于您从发布函数返回的游标的行为。您会看到与它们的查询匹配的数据的任何更改,但它们的查询永远不会更改

因此,在上面的例子中,如果用户订阅了一个稍后由另一个用户设为私有的列表,尽管 list.userId 将更改为不再满足条件的值,但发布的主体将不会重新运行,因此对 Todos 集合的查询({listId})不会更改。因此,第一个用户将继续看到他们不应该看到的项目。

但是,我们可以编写对集合中更改做出正确响应的出版物。为此,我们使用reywood:publish-composite包。

此包的工作原理是首先在一个集合上建立一个游标,然后使用第一个游标的结果在第二个集合上显式设置第二个级别的游标。该包在后台使用查询观察器来触发订阅更改和查询,以便在源数据更改时重新运行。

Meteor.publishComposite('todos.inList', function(listId) {
  new SimpleSchema({
    listId: {type: String}
  }).validate({ listId });

  const userId = this.userId;

  return {
    find() {
      const query = {
        _id: listId,
        $or: [{userId: {$exists: false}}, {userId}]
      };

      // We only need the _id field in this query, since it's only
      // used to drive the child queries to get the todos
      const options = {
        fields: { _id: 1 }
      };

      return Lists.find(query, options);
    },

    children: [{
      find(list) {
        return Todos.find({ listId: list._id }, { fields: Todos.publicFields });
      }
    }]
  };
});

在这个例子中,我们编写了一个复杂的查询来确保我们只在被允许查看列表时才找到列表,然后,对于我们找到的每个列表(根据访问权限可以是一次或零次),我们发布该列表的待办事项。发布组合负责在列表停止匹配原始查询或其他情况下停止和启动依赖游标。

复杂授权

我们还可以使用publish-composite在出版物中执行复杂的授权。例如,假设我们有一个Todos.admin.inList出版物,允许管理员绕过默认出版物的用户安全性,以设置admin标志。

我们可能希望编写

Meteor.publish('Todos.admin.inList', function({ listId }) {
  new SimpleSchema({
    listId: {type: String}
  }).validate({ listId });

  const user = Meteor.users.findOne(this.userId);

  if (user && user.admin) {
    // We don't need to worry about the list.userId changing this time
    return [
      Lists.find(listId),
      Todos.find({listId})
    ];
  } else {
    return this.ready();
  }
});

但是,由于上述相同的原因,如果用户的admin状态发生更改,出版物将不会重新运行。如果这种情况很可能发生并且需要反应式更改,那么我们需要使出版物具有反应性。我们可以通过与上面相同的技术来做到这一点,但是

Meteor.publishComposite('Todos.admin.inList', function(listId) {
  new SimpleSchema({
    listId: {type: String}
  }).validate({ listId });

  const userId = this.userId;
  return {
    find() {
      return Meteor.users.find({_id: userId, admin: true}, {fields: {admin: 1}});
    },
    children: [{
      find(user) {
        // We don't need to worry about the list.userId changing this time
        return Lists.find(listId);
      }
    },
    {
      find(user) {
        return Todos.find({listId});
      }
    }]
  };
});

请注意,我们显式地设置了Meteor.users查询字段,因为publish-composite将所有返回的游标发布到客户端,并在游标更改时重新运行子计算。

限制结果具有双重目的:它既可以防止敏感字段泄露给客户端,又可以将重新计算限制为仅相关的字段(即admin字段)。

使用低级 API 的自定义出版物

在我们迄今为止的所有示例中(除了使用Meteor.publishComposite()之外),我们都从我们的Meteor.publish()处理程序中返回了一个游标。这样做可以确保 Meteor 负责在服务器和客户端之间保持该游标内容同步的工作。但是,您还可以使用另一个 API 用于发布函数,该 API 更接近底层分布式数据协议 (DDP) 的工作方式。

DDP 使用三个主要消息来传达出版物中数据的更改:addedchangedremoved 消息。因此,我们也可以对出版物做同样的事情

Meteor.publish('custom-publication', function() {
  // We can add documents one at a time
  this.added('collection-name', 'id', {field: 'values'});

  // We can call ready to indicate to the client that the initial document sent has been sent
  this.ready();

  // We may respond to some 3rd party event and want to send notifications
  Meteor.setTimeout(() => {
    // If we want to modify a document that we've already added
    this.changed('collection-name', 'id', {field: 'new-value'});

    // Or if we don't want the client to see it any more
    this.removed('collection-name', 'id');
  });

  // It's very important to clean up things in the subscription's onStop handler
  this.onStop(() => {
    // Perhaps kill the connection with the 3rd party server
  });
});

从客户端的角度来看,以这种方式发布的数据看起来没有任何区别——实际上客户端无法知道差异,因为 DDP 消息是相同的。因此,即使您连接到某个深奥的数据源并对其进行镜像,在客户端上它也会显示为任何其他 Mongo 集合。

需要注意的一点是,如果您允许用户以这种方式修改您正在发布的“伪集合”中的数据,则您需要确保通过发布重新发布对它们的修改,以实现乐观的用户体验。

订阅生命周期

虽然您可以通过直观的理解在 Meteor 中使用出版物和订阅,但有时了解在您订阅数据时后台到底发生了什么很有用。

假设您有一个以下形式的出版物

Meteor.publish('Posts.all', function() {
  return Posts.find({}, {limit: 10});
});

然后,当客户端调用Meteor.subscribe('Posts.all')时,Meteor 内部会发生以下事件

  1. 客户端通过 DDP 发送带有订阅名称的sub消息。

  2. 服务器通过运行发布处理程序函数来启动订阅。

  3. 发布处理程序识别返回值是一个游标。这为发布游标启用了一种便捷模式。

  4. 服务器在该游标上设置查询观察器,除非服务器上(对于任何用户)已经存在此类观察器,否则将重用该观察器。

  5. 观察器获取与游标匹配的当前文档集,并将它们传递回订阅(通过this.added()回调)。

  6. 订阅将添加的文档传递给订阅客户端的连接合并框,该合并框是已发布到此特定客户端的文档的服务器端缓存。每个文档都与客户端已知的任何现有文档版本合并,并发送added(如果文档对客户端是新的)或changed(如果已知但此订阅正在添加或更改字段)DDP 消息。

    请注意,合并框在顶级字段级别运行,因此如果两个订阅发布嵌套字段(例如,sub1 发布doc.a.b = 7,sub2 发布doc.a.c = 8),则“合并”的文档可能看起来不像您期望的那样(在这种情况下,doc.a = {c: 8},如果 sub2 发生在第二位)。

  7. 出版物调用.ready()回调,该回调将 DDP ready消息发送到客户端。客户端上的订阅句柄被标记为已准备就绪。

  8. 观察器观察查询。通常,它使用 MongoDB 的 Oplog来注意影响查询的更改。如果它看到相关更改,例如新的匹配文档或匹配文档上的字段更改,它会调用订阅(通过.added().changed().removed()),这又将更改发送到合并框,然后通过 DDP 发送到客户端。

这种情况将持续到客户端停止订阅,从而触发以下行为

  1. 客户端发送unsub DDP 消息。

  2. 服务器停止其内部订阅对象,触发以下效果

  3. 发布处理程序设置的任何this.onStop()回调都将运行。在这种情况下,它是从处理程序返回游标时设置的单个自动回调,它会停止查询观察器并在必要时清理它。

  4. 此订阅跟踪的所有文档都将从合并框中删除,这可能也可能不会意味着它们也从客户端中删除。

  5. nosub消息被发送到客户端以指示订阅已停止。

使用 REST API

出版物和订阅是在 Meteor 的 DDP 协议中处理数据的主要方式,但许多数据源在其 API 中使用流行的 REST 协议。能够在这两者之间进行转换很有用。

使用发布加载来自 REST 端点的数据

作为使用低级 API的具体示例,请考虑您拥有某些提供有价值的更改数据集的第三方 REST 端点的情况。您如何使该数据可用?

一种选择是提供一个代理到端点的 Method,客户端负责轮询和处理传入的更改数据。因此,客户端需要负责维护数据的本地数据缓存,在发生更改时更新 UI 等。虽然这可能是可能的(例如,您可以使用 Local Collection 来存储轮询的数据),但创建执行此轮询的发布更简单,也更自然。客户端。

将轮询的 REST 端点转换为某种模式如下所示

const POLL_INTERVAL = 5000;

Meteor.publish('polled-publication', async function() {
    const publishedKeys = {};

    const poll = async () => {
        // Let's assume the data comes back as an array of JSON documents, with an _id field
            const response = await fetch(REST_URL, REST_OPTIONS);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            } else {
                data = await response.json();
                data.forEach((doc) => {
                    if (publishedKeys[doc._id]) {
                        this.changed(COLLECTION_NAME, doc._id, doc);
                    } else {
                        publishedKeys[doc._id] = true;
                        this.added(COLLECTION_NAME, doc._id, doc);
                    }
                });
            }
    };

    await poll();
    this.ready();

    const interval = Meteor.setInterval(poll, POLL_INTERVAL);

    this.onStop(() => {
        Meteor.clearInterval(interval);
    });
});

事情可能会变得更加复杂;例如,您可能希望处理文档被删除的情况,或者在多个用户之间共享轮询工作(在轮询的数据不属于该用户私有数据的情况下),而不是对每个感兴趣的用户执行完全相同的轮询。

将发布作为 REST 端点访问

当您希望将数据发布到第三方(通常通过 REST)使用时,会发生相反的情况。如果我们要发布的数据与我们已经通过发布发布的数据相同,那么我们可以使用simple:rest包来做到这一点。

在 Todos 示例应用程序中,我们已经这样做了,您现在可以通过 HTTP 访问我们的出版物

$ curl localhost:3000/publications/lists.public
{
  "Lists": [
    {
      "_id": "rBt5iZQnDpRxypu68",
      "name": "Meteor Principles",
      "incompleteCount": 7
    },
    {
      "_id": "Qzc2FjjcfzDy3GdsG",
      "name": "Languages",
      "incompleteCount": 9
    },
    {
      "_id": "TXfWkSkoMy6NByGNL",
      "name": "Favorite Scientists",
      "incompleteCount": 6
    }
  ]
}

您还可以访问经过身份验证的出版物(例如lists.private)。假设我们(通过 Web UI)已注册为[email protected],密码为password,并创建了一个私有列表。然后我们可以按如下方式访问它

# First, we need to "login" on the commandline to get an access token
$ curl localhost:3000/users/login  -H "Content-Type: application/json" --data '{"email": "[email protected]", "password": "password"}'
{
  "id": "wq5oLMLi2KMHy5rR6",
  "token": "6PN4EIlwxuVua9PFoaImEP9qzysY64zM6AfpBJCE6bs",
  "tokenExpires": "2016-02-21T02:27:19.425Z"
}

# Then, we can make an authenticated API call
$ curl localhost:3000/publications/lists.private -H "Authorization: Bearer 6PN4EIlwxuVua9PFoaImEP9qzysY64zM6AfpBJCE6bs"
{
  "Lists": [
    {
      "_id": "92XAn3rWhjmPEga4P",
      "name": "My Private List",
      "incompleteCount": 5,
      "userId": "wq5oLMLi2KMHy5rR6"
    }
  ]
}

扩展更新

如前所述,Meteor 使用 MongoDB 的 Oplog 来识别要应用于哪个发布的更改。每个数据库更改都会由每个 Meteor 服务器处理,因此频繁更改会导致整个服务器的 CPU 使用率很高。同时,您的数据库将承受更高的负载,因为所有服务器都会不断从 oplog 中获取数据。

为了解决此问题,您可以使用cultofcoders:redis-oplog包,该包完全放弃使用 MongoDB 的 Oplog 并将通信转移到 Redis 的 Pub/Sub 系统。

注意事项

  1. 如果您的应用程序较小或数据变化不大,则可能不需要它。
  2. 您必须维护另一个数据库Redis
  3. 在 Meteor 实例外部发生的更改必须手动提交到 Redis

好处

  1. 降低服务器 CPU 和 MongoDB 的负载。
  2. 向后兼容(不需要更改您的发布/订阅代码)。
  3. 更好地控制哪些更新应触发反应性。
  4. 您可以使用未启用 oplog 的 MongoDB 数据库。
  5. 使用Vent完全控制反应性。
meteor add cultofcoders:redis-oplog
meteor add disable-oplog

在您的settings.json文件中

{
    "redisOplog": {}
}
# Start Redis, then run Meteor
meteor run --settings settings.json

在此处详细了解redis-oploghttps://github.com/cult-of-coders/redis-oplog

在 GitHub 上编辑
// 搜索框