数据装载
通过阅读本文,你会了解到:
- 在流星平台上什么是发布和订阅。
- 如何在服务器端定义一个发布。
- 在客户端(哪些模版中)的何处进行订阅。
- 管理订阅的有用的模式。
- 如何响应式地发布相关数据。
- 在面对响应式改变时如何确保你的订阅是安全的。
- 如何使用底层的发布API来发布任意的数据。
- 当订阅某个发布时究竟发生了什么。
- 如何将某个第三方的REST终端转换为一个发布。
- 如何将某个应用中的发布转化为一个REST终端。
发布和订阅
传统基于HTTP的web应用程序,客户端和服务器端之间的通讯采用的是“请求-响应”的方式。通常客户端会发起REST的HTTP请求到服务器,接着在响应中接收HTML或JSON数据,对于服务器而言,当后台发生变化时,没有办法将数据“推送”到客户端。
流星构建于分布式数据协议(DDP)之上,可以使数据进行双向传输。构建一个流星应用不需要你设置REST终端以串行化数据并发送之。取而代之,你会创建发布终端用来将数据从服务器端推送到客户端。
在流星中,一个发布是在服务器端具有名称的API,用来构造一组数据并发送到客户端。一个客户端初始化一个订阅来连接到一个发布,并接受那些数据。这些数据由订阅初始化时发送的第一批数据和接着发布数据变化的增量更新来组成。
所以一个订阅能够被想象为一组随着时间变化的数据。通常,一个订阅“桥接”着一个服务器端 MongoDB 集合和那个集合的客户端 Minimongo 缓存。你可以把一个订阅想象成一个管道连接着具有客户端版本的、“真实的”集合子集,和通过服务器上的最新信息的持续地更新。
定义一个发布
一个发布应该被定义在服务器端运行的文件中。例如,在Todos示例应用中,我们想要发布一组共用的lists给所有的用户:
Meteor.publish('Lists.public', function() {
return Lists.find({
userId: {$exists: false}
}, {
fields: Lists.publicFields
});
});
理解这段代码需要注意几点。首先,我们对该发布命名其为唯一的字符串 Lists.public
,这就是我们将来如何从客户端机型访问它的标示。其次,我们从发布函数中返回了一个Mongo 游标 。注意该游标被限定为只返回特定的集合字段,更详细的描述请参见安全一章
这意味着发布能够确保一组符合查询的数据能够对任意订阅它的客户端而言是可用的。在这种情况下是指所有不具有userId
的list数据。所以,当该订阅被打开,在客户端的Lists
集合将具有服务器上Lists
集合的所有公共的list数据。在Todos应用程序这个特定的例子中,当应用开始时订阅就被初始化并且不会停止,稍后的章节将会讨论订阅的生命周期
每个发布都有两种类型的参数:
this
上下文具有关于当前DDP连接的信息。例如,你可以通过this.userId
访问当前用户的_id
。- 对于该发布的参数是当调用
Meteor.subscribe
时能够被传递进来。
注意:我们在
this
中访问上下文时,应该对于发布使用function () {}
形式,而不是使用ES2015的() => {}
。你可以为发布文件禁用箭头函数,可以使用eslint-disable prefer-arrow-callback
的linting规则来达到效果。未来的发布API的版本将会和ES2015一起工作得更好。
下面的发布中,载入了私有的list,我们需要使用this.userId
来获得只属于该特定用户的todo list。
Meteor.publish('Lists.private', function() {
if (!this.userId) {
return this.ready();
}
return Lists.find({
userId: this.userId
}, {
fields: Lists.publicFields
});
});
要感谢由DDP和流星的账号系统提供的保证性,上述发布可以很有信心的表示它只会发布只属于该用户的私有list。注意到 发布 会在用户登出(或再次回来)的时候重新运行,就是说被发布的这组私有list会随着激活的用户改变而改变。
在登出用户的情况下,我们显式地调用this.ready()
,表示我们已经发送所有初始化需要发送的数据(这种情况下没有任何的数据)到该订阅。要知道如果你不从订阅中返回一个游标,也不调用this.ready()
,那么用户的订阅将永远不会准备好,他们会一直看到装载的状态。
这里有个具有一个命名的参数的发布示例。注意非常重要的一点是需要检测通过网络传递的参数类型。
Meteor.publish('Todos.inList', function(listId) {
// 我们需要检测`listId`的类型是否符合我们的预期
new SimpleSchema({
listId: {type: String}
}).validate({ listId });
// ...
});
当我们在客户端对该发布进行订阅时,我们通过Meteor.subscribe()
调用来提供这个参数:
Meteor.subscribe('Todos.inList', list._id);
组织发布
将发布和目标的特征放置在一起是具有意义的。例如,有时发布提供非常特定的数据,这些数据只对于某个视图才有用。这种情况下,在相同模组或目录下放置该发布来作为该视图代码,则及其有意义。
经常性的,某个发布会呈现更通用化的趋势。例如在Todos示例应用中,我们创建了一个Todso.inList
发布,它会发布某个list中所有的todos。虽然在该应用程序里,我们只在一个地方使用了(在Lists_show
模版里),但在一个大型的应用中,很大的机会我们需要在应用程序别的地方去访问某个list的所有todos。所以把该发布放在todos
包里会是个好主意。
数据订阅
要使用发布,你需要创建个在客户端的订阅。你可以通过订阅的名称来调用Meteor.subscribe()
。当你这么做的时候,这会开启一个到发布的订阅,并且服务器通过该订阅开始下发数据来确保客户端的集合包含着发布数据的日期副本。
Meteor.subscribe()
also returns a "subscription handle" with a property called .ready()
. This is a reactive function that returns true
when the publication is marked ready (either you call this.ready()
explicitly, or the initial contents of a returned cursor are sent over).Meteor.subscribe()
还会返回一个“订阅处理”,其具有一个名为.ready()
的属性。这是一个响应式的函数,当发布标记为准备(无论是你显式地调用this.ready()
还是返回的游标的初始化的内容被发送)时,其会返回true
。
const handle = Meteor.subscribe('Lists.public');
停止订阅
订阅处理还有另一个重要的属性,就是.stop()
方法。当你正进行订阅,保证在你已经完成时,总是要在订阅上调用.stop()
。这能确保由订阅发送的文档从你本地Minimongo缓存中得到清理并且服务器停止订阅所需的服务。
然而,如果你是在一个响应式的上下文中(如某个autorun
,或React中的getMeteorData
)有条件地调用Meteor.subscribe()
或者通过在某个Blaze组件中调用this.subscribe()
,那么流星的响应式系统会自动为你在合适的时候调用this.stop()
。
在用户界面组件中进行订阅
将订阅放置在足够靠近其数据使用的地方,这可以减少所谓的“长距离动作”(带来的影响)并容易理解应用程序的数据流。如果该订阅和获取是分离的,那么对于如何以及为什么订阅会产生改变(例如改变其参数),而这样的改变会影响到订阅的内容,总的来说,这并不总是让人容易理解。
在实践中这意味着你需要在Blaze的组件里放置你的订阅的调用。在Blaze中,最好是是在onCreated()
的回调中进行调用:
Template.Lists_show_page.onCreated(function() {
this.getListId = () => FlowRouter.getParam('_id');
this.autorun(() => {
this.subscribe('Todos.inList', this.getListId());
});
});
在上述代码片段中,我们可以看到两个对于在Blaze模版中进行订阅的非常重要的技术:
调用
this.subscribe()
(而不是Meteor.subscribe
),这会附加一个特殊的叫做subscriptionsReady()
方法到模版实例上,当在此模版中所有的订阅都已经准备就绪时,其会返回true。调用
this.autorun
会设置一个响应式的上下文,无论何时,响应式函数this.getListId()
发生改变的时候,其会重新初始化该订阅。
要阅读更多关于Blaze订阅的信息,请参见Blaze 一章,要阅读更多关于在用户界面组件中跟踪装载状态,请参见用户界面一章。
获取数据
订阅数据放置在客户端的集合中。要在用户界面上使用该数据,你需要查询客户端的集合。当我们着么做的时候,有一些重要的规则是我们需要去遵守的。
总是使用特定的查询进行数据查询
如果发布一组数据子集,你可能试图简单地查询某个集合中所有可能的数据(如,Lists.find()
)以此为了能在客户端获得该子集,这种不重新指定Mongo选择器的做法通常是你在第一次发布数据的时候所做的。
但是如果你这么做了,那么你会陷入你自己造成的麻烦里-如果另一个订阅也把数据推送到相同的集合里,那么从Lists.find()
中返回的数据可能并不再是你所预期的那样。
当两个订阅之间发生改变时,两个订阅的装载会有一段周期(参见参数改变时的发布行为),例如分页就正好是这么一种情况。
在靠近订阅的地方获取数据
因为相同的理由我们在组件的第一个地方进行订阅---为避免所谓的“长距离动作”并且让人容易理解数据从何处而来。一种通用的模式是在父模版中获取数据,然后将其传递到一个“纯粹的”子组件,我们可以在用户界面一章中进行查看。
请注意对于第二条规则有一些例外。常见的就是Meteor.user()
---虽然严格而言这也是订阅(通常是自动的),但是将其作为参数在不同的组件之间进行传递是典型的过度复杂(的行为)。然而请牢记,这样的例外不要在太多的地方使用,因为这会造成组件很难测试。
全局订阅
有个地方你可能会尝试不在组件中进行订阅,当应用需要访问数据时,那就是你知道(这些数据)总是会被用到。例如,某个对于用户对象[账号一章]额外字段的订阅会在每个屏幕上都会被用到。
然而,使用一个排版组件(包裹着所有的其它组件)来订阅这个订阅通常是个好主意。当然最好是能够对此提供一致性,这会组成一个更有弹性的系统如果你曾经决定某个屏幕不需要这样的数据。
数据装载模式
纵观流星的应用程序,在客户端数据装载和管理的一些通用模式是值得学习的。我们将在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'}`);
});
当我们尝试显示数据给用户,当我们显示装载界面时,使用该信息可以使我们的程序更稳定。
响应式地改变订阅参数
我们已经看到这样一个例子—使用一个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’)
发生了改变),其它常用的响应式数据源还有:
- 模版数据上下文(能够通过
Template.currentData()
响应式地访问)。 - 当前用户状态(
Meteor.user()
和Meteor.loggingIn()
)。 - 应用程序中其它特定的客户端数据存储的内容。
技术上讲,当这些响应式数据源发生改变时发生了什么,如下所示:
- 响应式数据源无效了该autorun的计算(标记它以至于它在下次Tracker flush周期里重新运行)。
- 订阅侦测到这些,并且给予在下次计算运行中的任意可能性,标记它自己为已经销毁状态。
- 该计算重新运行,
.subscribe()
被重新重新调用,要么给到相同的参数,要么给到不同的参数。 - 如果该订阅是通过相同参数运行,那么该“新的”订阅会发现已经被标记为已经销毁的旧的标签的订阅在那里,由于相同的数据已经准备好,那么只要简单地重新使用。
- 如果该订阅是通过不同的参数来运行的,那么当一个新的订阅被创建,其会连接到服务器上的相应的发布。
- 在flush周期的最后(如,计算完成了重新运行),旧的订阅会检查它是否是要被重用的,如果不是,发送一个消息到服务器来告知服务器关闭它。
上述第4步是一个重要的细节—系统很聪明地知道,如果autorun重新运行并且订阅有着完全相同的参数(值),那么系统不会重新去订阅。即使新的订阅被在模版层级的任意地方创建出来,还是相同的效果。例如,如果一个用户在两个页面之间进行浏览,都订阅了完全相同的订阅,相同的机制会得到应用,也就是,没有不必要的订阅会发生。
当参数(值)改变时的发布行为
当新的订阅开始以及旧的订阅停止时,在服务器端究竟发生了什么,值得我们去了解。
服务器显式地等待直到所有新的订阅的数据被下发(新的订阅准备好了),然后,再从旧的订阅中移除数据。这里的想法是为了避免闪烁—你可以,如果想要的话,继续显示旧的订阅的数据直到新的数据准备好,那么立即切换到新的订阅的完整的数据集。
总而言之,当订阅改变,有段时间,你在客户端会过度-订阅并且客户端的数据比要求的要多。这是一个非常重要的原因就是为什么对于订阅过的数据你总是应该存取一样的数据(不要“过度-获取”)。
分页订阅
数据访问的一个非常通用的模式就是分页。这是指一次获取排序过的列表数据只获取一“页”—典型地是几个项,假设二十个。
有两种类型的分页是常用的,一种是“一页接着一页”—一次只显示一页,起始于某个偏移量(这是用户可以控制的),另一种是“无限-滚动”,你可以显示增量的项,由于用户移动该列表(这是典型的“feed”风格的用户界面)。
本节中,我们将考虑对于第二种的发布/订阅技术,无限-滚动风格的分页。一页接着一页的技术在流星中处理起来有些麻烦,这是因为在客户端很难计算其偏移量。如果你需要着么做,你可以遵循我们这里使用的相同的技术,并且使用percolate:find-from-publication
包来保持对于从哪个发布而来的记录数据。
在一个无限滚动发布中,我们只需要添加一个新参数到我们的订阅用来控制多少项用来加载。假设我们需要分页todo项,在我们的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}`)
具有响应式存储的客户端数据
在流星里,持久化或分享数据是通过连线发布而来的。然而,有些数据类型并不需要在用户间被持久化或分享。例如,当前用户的“登录与否”,或他们现在正在浏览的(页面的)路由(信息)。
虽然客户端的状态经常经常包含着单个模版的状态(并且在需要时作为参数在模版层级之间进行向下传递),有时你有“全局”状态的需求,这种全局状态是在不同的模版层级的不相关的部分进行共享的。
通常这样的状态是存储在一个全局单例对象,我们能够调用的一个存储。一个单例是一个数据结构仅仅是一个单一的逻辑存在的拷贝。当前用户和上述路由是典型的全局单例的例子。
存储类型
在流星中,最好是把存储作为响应式数据源,因为这样它们就可以很自然融入到剩余的生态系统中。有几个包你可以用来作为存储(的数据源)。
如果存储是一维的,你可以使用ReactiveVar
来存储它(由[reactive-var
(https://atmopsherejs.com/meteor/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应用中,当我们渲染一个todo list,我们需要list的自身,也需要属于该list的一组todos。
一种你可以使用的方式是从你的发布函数中返回多个游标:
Meteor.publish('Todos.inList', function(listId) {
new SimpleSchema({
listId: {type: String}
}).validate({ listId });
const list = List.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.该list不存在,或用户不允许看到它。
// In either case, make it appear like there is no list.无论何种情况,让它看上去就像没有list一样。
return this.ready();
}
});
然而,这个例子可能并不像你预期的那样工作。原因是在服务器端的响应性并不像在客户端那样工作。在客户端,如果任何事物在某个响应式函数改变,整个函数会重新运行,并且结果是相当直观的。
而在服务器端,响应性会被局限在发布函数中游标的行为。你将会看到符合它们查询的数据的任意改变,但是它们的查询将永远不会改变。
所以在上面的例子中,如果某个用户订阅一个list,而这个list稍后被另一个用户变成私有的,虽然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我们只需要在这个查询中的_id,由于它只被
// used to drive the child queries to get the todos用来驱动子查询来获得todos
const options = {
fields: { _id: 1 }
};
return Lists.find(query, options);
},
children: [{
find(list) {
return Todos.find({ listId: list._id }, { fields: Todos.publicFields });
}
}]
};
});
这个例子中,我们写了一个复杂的查询来确保我们只会找到一个list,是我们允许被看到的,接着,一旦我们找到(根据访问是1或0次)的每个list,我们发布该list的todos。发布组合(包)会照料停止和启动相关的游标,如果该list停止符合原来的查询,启动也是一样。
复杂的授权
我们也可以使用publish-composite
来执行发布中的复杂授权。例如,考虑如果我们有一个Todos.admin.inList
发布允许一个管理员(user对象上有一个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({userId, admin: true});
},
children: [{
find() {
// We don't need to worry about the list.userId changing this time
return [
Lists.find(listId),
Todos.find({listId})
];
}
}]
};
});
通过底层API自定义发布
目前为止我们所有的例子中(除了使用Meteor.publishComposite()
)我们从Meteor.publish()
处理器返回一个游标。这么做确保了流星会保持游标的内容的同步在服务器和客户端之间。然而,另一个API你可以用在发布函数上是和底层分布式数据协议(DDP)工作方式很接近的一种工作方式。
DDP使用三个主要的消息来沟通发布数据的变化: added
, changed
和 removed
消息.
Meteor.publish('custom-publication', function() {
// We can add documents one at a time我们可以一次添加一个文档
this.added('collection-name', 'id', {field: 'values'});
// 我们可以调用准备来指示客户端初始文档已经发送完成
this.ready();
// 我们也可以响应一些第三方事件并且发送通知
Meteor.setTimeout(() => {
// 如果我们需要改变一个已经添加的文档
this.changed('collection-name', 'id', {field: 'new-value'});
// 或者我们不想再让客户端看到它
this.removed('collection-name', 'id');
});
// 在订阅的onStop处理器中清理是非常重要的
this.onStop(() => {
// 可能是断开第三方服务器的连接
});
});
从客户端角度而言,这样的数据发布没有任何的区别—其实客户端也确实不会知道这些区别,因为DDP的消息都是一样的。所以即使你连接到,镜像了,晦涩的数据源,在客户端这些数据的呈现就像其它的Mongo集合一样。
这里要申明一点,如果你允许用户在以这种方式发布的“伪-集合”中修改数据,你会想要确保要通过该发布来重新这些修改,来达到一种乐观的用户体验。
订阅的生命周期
虽然你能通过一种直观的理解在流星中使用发布和订阅,有时知道当你订阅数据时究竟发生了些什么也是很有用的。
假设你有一个简单的发布如下形式:
Meteor.publish('Posts.all', function() {
return Posts.find({}, {limit: 10});
});
接着当一个客户端调用Meteor.subscribe('Posts.all')
时,下列步骤会在流星内部发生:
客户端通过DDP发送一个订阅名字的
sub
消息服务器通过运行该发布的处理器函数启动一个订阅。
发布处理器识别返回值是一个游标。这对于发布游标激活了一种方便的模式。
服务器在该游标上设置一个查询观察者,除非已经有这样的一个观察者存在在服务器上(对任意用户而言),这种情况下观察者会被重用。
观察者获取符合该游标的一组文档,并且把它们传递回该订阅(通过
this.added()
的回调)。订阅传递添加的文档到订阅的客户端的连接的mergebox,这是一个服务器上的文档的缓存,这些文档是被发布到特定的客户端的。每个文档是通过任何已经存在的、客户端知道的文档版本进行合并的,并且一个
added
(如果文档对客户端而言是新的)或者changed
(如果这是已知的但是这个订阅已经添加或改变字段)DDP消息会被发送。
注意mergebox操作只发生在顶层,所以如果两个订阅发布是嵌套的(如,sub1发布了doc.a.b = 7
,sub2发布了doc.a.c = 8
),那么合并的文档或许并不是想你所期待的那样(这种情况下是doc.a = {c:8}
,如果sub2发生在第二次)。
该发布调用
.ready()
回调,会发送DDPready
消息到客户端。该订阅处理在客户端被标记为准备。观察者观察查询。通常,它使用 MongoDB's Oplog来通知影响该查询的改变。如果它看到一个相关的改变,如一个新的符合的文档或在一个符合的文档字段上的改变,会调用订阅中的(通过
.added()
,.changed()
或.removed()
),就会再次发送变化到mergebox,接着通过DDP发送到客户端。
这会持续直到客户端停止 该订阅,接着出发以下行为:
客户端发送
unsub
DDP消息。服务器停止它的内部订阅对象,出发以下效果:
任何
this.onStop()
回调设置,这是通过发布处理器运行来完成的。这种情况下,它有一个单独的自动回调设置,当从处理器返回一个游标时,处理器停止查询观察者并在有需要时进行清理。所有文档通过这个订阅跟踪会从mergebox里进行移除,也许它们也会从客户端被移除掉。
nosub
message被发送到客户端来指示该订阅已经停止。
和REST APIs一起工作
发布和订阅是通过流星DDP协议处理数据的主要方式,单很多数据源使用流行的REST协议作为它们的API。因此,这两者之间的互相转换就很有用了。
从一个REST终端通过一个发布装载数据
作为使用[底层API]有力的例子,考虑这样一种情况,你需要一些第三方REST终端来提供一组变化的数据,而这些数据对于你的用户很有价值。你如何使得这些数据成为可用?
一种选择可以提供一个Method简单地代理到该终端,可以由客户端承担职责来轮询并处理响应的数据改变。所以处理保持本地数据的缓存,当其变化时更新用户界面就成为客户端的问题。虽然这是可能的(你应该使用一个本地集合来存储轮询到的数据),而更简单的,并且更自然的方式是创建一个发布来为客户端进行这样的轮询。
转换轮询的REST终端的一种方式或许如下:
const POLL_INTERVAL = 5000;
Meteor.publish('polled-publication', function() {
const publishedKeys = {};
const poll = () => {
// Let's assume the data comes back as an array of JSON documents, with an _id field, for simplicity
const data = HTTP.get(REST_URL, REST_OPTIONS);
data.forEach((doc) => {
if (publishedKeys[doc._id]) {
this.changed(COLLECTION_NAME, doc._id, doc);
} else {
publishedKeys[doc._id] = true;
if (publishedKeys[doc._id]) {
this.added(COLLECTION_NAME, doc._id, doc);
}
}
});
};
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
}
]
}
假设我们已经注册了(通过web界面)[email protected]
,密码是password
,并且创建了一个私有的list。那么我们可以这样进行访问:
# 首先,我们需要通过命令行进行“登录”以获取一个访问令牌
$ 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"
}
# 接着,我们可以发起一个对于需要验证的API的请求
$ curl localhost:3000/publications/Lists.private -H "Authorization: Bearer 6PN4EIlwxuVua9PFoaImEP9qzysY64zM6AfpBJCE6bs"
{
"Lists": [
{
"_id": "92XAn3rWhjmPEga4P",
"name": "My Private List",
"incompleteCount": 5,
"userId": "wq5oLMLi2KMHy5rR6"
}
]
}